【问题标题】:Javascript : setTimeout and interface freezingJavascript:setTimeout 和界面冻结
【发布时间】:2013-04-05 01:59:22
【问题描述】:

上下文

我有大约 10 个复杂的图表,每个图表需要 5 秒来刷新。如果我对这 10 个图表进行循环,刷新大约需要 50 秒。在这 50 秒内,用户可以移动滚动条。如果滚动条被移动,刷新必须停止,当滚动条停止移动时,刷新再次发生。

我在循环中使用 setTimeout 函数让界面刷新。 算法是:

  • 渲染第一张图
  • setTimeout(渲染第二张图,200)
  • 当第二个图被渲染时,在 200 毫秒内渲染第三个,以此类推

setTimeout 允许我们捕获滚动条事件并清除下一次刷新的超时时间以避免在移动滚动条之前等待 50 秒...

问题是它不能随时运行。

获取以下简单代码(您可以在这个小提琴中尝试:http://jsfiddle.net/BwNca/5/):

HTML:

<div id="test" style="width: 300px;height:300px; background-color: red;">

</div>
<input type="text" id="value" />
<input type="text" id="value2" />

Javascript:

var i = 0;
var j = 0;
var timeout;
var clicked = false;

// simulate the scrollbar update : each time mouse move is equivalent to a scrollbar move
document.getElementById("test").onmousemove = function() {

    // ignore first move (because onclick send a mousemove event)
    if (clicked) {
        clicked = false;
        return;
    }

    document.getElementById("value").value = i++; 
    clearTimeout(timeout);
}

// a click simulates the drawing of the graphs
document.getElementById("test").onclick = function() {
    // ignore multiple click
    if (clicked) return;

    complexAlgorithm(1000);    
    clicked = true;   
}

// simulate a complexe algorithm which takes some time to execute (the graph drawing)
function complexAlgorithm(milliseconds) {
  var start = new Date().getTime();
  for (var i = 0; i < 1e7; i++) {
    if ((new Date().getTime() - start) > milliseconds){
      break;
    }
  }   

  document.getElementById("value2").value = j++;

  // launch the next graph drawing
  timeout =  setTimeout(function() {complexAlgorithm(1000);}, 1);
}

代码是这样的:

  • 当您将鼠标移入红色 div 时,它会更新一个计数器
  • 当你点击红色的 div 时,它会模拟 1 秒的大处理(因此由于 javascript 单线程,它会冻结界面)
  • 冻结后,等待1ms,重新模拟处理等直到鼠标再次移动
  • 当鼠标再次移动时,它会打破超时以避免无限循环。

问题

当您单击一次并在冻结期间移动鼠标时,我在想当 setTimeout 发生时将执行的下一个代码是 mousemove 事件的代码(因此它将取消超时和冻结) 但有时由于 mouvemove 事件,点击计数器会获得 2 分或更多分,而不是仅获得 1 分...

此测试的结论:setTimeout 函数并不总是在 mousemove 事件期间释放资源来执行代码,但有时会在执行另一个代码之前保留线程并在 settimeout 回调中执行代码。

这样做的影响是,在我们的实际示例中,用户可以等待 10 秒(渲染 2 个图表)而不是等待 5 秒再使用滚动条。这很烦人,我们需要避免这种情况,并确保在渲染阶段移动滚动条时只渲染一个图形(而其他图形被取消)。

鼠标移动时如何确保打破超时?

PS:在下面的简单示例中,如果您将超时更新为 200 毫秒,则一切都可以完美运行,但这不是一个可接受的解决方案(真正的问题更复杂,问题发生在 200 毫秒的计时器和复杂的接口上)。请不要提供“优化图形渲染”的解决方案,这不是这里的问题。

编辑:cernunnos 对问题有更好的解释: 此外,通过“阻塞”循环中的进程,您可以确保在该循环完成之前无法处理任何事件,因此任何事件只会在每个循环执行之间被处理(并清除超时)(因此为什么您有时必须等待 2 次或更多次完整执行才能中断)

问题恰好包含在粗体字中:我想确保在我想要的时候中断执行,而不是在中断之前等待 2 次或更多次完整的执行


第二次编辑:

总结:采用这个 jsfiddle:http://jsfiddle.net/BwNca/5/(上面的代码)。

更新这个 jsfiddle 并提供解决方案:

鼠标在红色 div 上移动。然后单击并继续移动:右计数器必须只加一次。但有时它会在第一个计数器再次运行之前加注 2 或 3 次...这是问题所在,它必须只加注一次!

【问题讨论】:

  • 问题与setTimeout无关。问题是您正在运行一个大循环。你不能用循环创建睡眠方法,它会锁定浏览器。
  • 我在这段代码中看到的唯一循环是浪费1秒的时间来冻结界面(它模拟了一个更复杂的算法,我在这里无法复制),这不是我认为的问题
  • 当您故意冻结浏览器时排队的事件将在timeout = setTimeout(function() {sleep(1000);}, 1); 之前触发,这意味着clearTimeout(timeout); 将在设置超时之前运行。此外,您为什么要让浏览器冻结?这不是好的做法,因为它影响的不仅仅是当前标签。
  • 好吧,我认为杰罗姆不想让浏览器冻结。这不是重点,只是为了测试目的。
  • 我不认为你可以强制事件循环支持某些事件——它们的异步性超出了脚本的控制。例如在 Opera 中,我无法重现您的问题(尽管我可以看到)。

标签: javascript settimeout


【解决方案1】:

这里的大问题是 setTimeout 一旦开始就无法预测,尤其是当它执行一些繁重的任务时。

您可以在此处查看演示: http://jsfiddle.net/wao20/C9WBg/

var secTmr = setTimeout(function(){
    $('#display').append('Timeout Cleared > ');
    clearTimeout(secTmr);        

    // this will always shown
    $('#display').append('I\'m still here! ');
}, 100);

您可以做两件事来尽量减少对浏览器性能的影响。

存储setTimeoutID的所有实例,当你想停止时循环遍历它

var timers = []

// When start the worker thread
timers.push( setTimeout(function () { sleep(1000);}, 1) );

// When you try to clear 
while (timers.length > 0) {
     clearTimeout(timers.pop());
}

在您尝试停止进程时设置一个标志,并在您的工作线程中检查该标志,以防 clearTimeout 未能停止计时器

// Your flag 
var STOPForTheLoveOfGod = false;

// When you try to stop 
STOPForTheLoveOfGod = true; 
while (timers.length > 0) {       
     clearTimeout(timers.pop()); 
}

// Inside the for loop in the sleep function 
function sleep(milliseconds) {
    var start = new Date().getTime();
    for (var i = 0; i < 1e7; i++) {
        if (STOPForTheLoveOfGod) {
            break;
        }       

        // ...  
    }
}

您可以试用这个新脚本。 http://jsfiddle.net/wao20/7PPpS/4/

【讨论】:

    【解决方案2】:

    我可能已经理解了这个问题,但假设您尝试在单击至少 1 秒后阻止界面,并通过移动鼠标(至少 1 秒后)解除阻止:

    这不是一个好的睡眠实现,因为您要让进程一直运行(什么都不做!= 睡眠),这会导致资源浪费。

    为什么不创建一个叠加层(半透明/完全透明的 div),将其放在界面的其余部分(位置固定、全宽和全高)之上,并使用它来防止与底层界面的任何交互。然后在条件合适时销毁它(一秒钟过去了,用户移动了鼠标)。

    这更像是睡眠(有一些初始处理时间,但随后会释放处理器一段给定的时间),应该可以帮助您实现所需的行为(假设我理解正确)。

    它还有一个额外的好处,就是允许您给用户一些视觉提示,表明正在进行某些处理。

    编辑: 此外,通过“阻塞”循环中的进程,您可以确保在该循环完成之前无法处理任何事件,因此任何事件只会在每个循环执行之间被处理(并清除超时)(因此有时你有在中断之前等待 2 次或更多次完整执行)。

    【讨论】:

    • 感谢您的回答,我已经编辑了我的帖子以反映我不想睡觉而是模拟一个复杂的算法...问题是您在答案的最后一行解释的!为什么在中断之前我们可以有 2 次或更多完整的执行(我需要总是在只执行一次后中断)
    • 除非你在“复杂”循环中实现机制来中断它的执行,一旦它开始就会结束。在您的示例中,单击并立即移动鼠标将导致它仅执行一次,本质上,移动鼠标的作用是“停止下一次执行”命令而不是“中断当前执行”,我不知道您是否可以实现后者用你目前的方法。
    【解决方案3】:

    令人惊讶的是,当您 setTimeout();之后您可以输入支票。一个变量为真然后丢弃等待,或者丢弃它。现在有一种方法可以让您检查是否使用滚动条进行滚动。在您使用该方法在变量中检查它是否为真之后,您会发现这将在滚动条时重复无限次,使许多执行时间为 5 秒。要解决此问题,请等待 1 秒以确保它不会过度重复。欢迎您:)

    【讨论】:

      【解决方案4】:

      任何长时间运行的函数都会占用您的浏览器窗口。考虑使用 WebWorkers 将您的 complexAlgorithm() 移到主 JavaScript 代码之外。

      【讨论】:

        【解决方案5】:

        答案就在你的问题中

        ...刷新必须停止,当滚动条停止移动时, 再次发生刷新。

        你应该以这样的方式编写 complexAlgorithm,这样你几乎可以立即在中间刹车(当你知道你将不得不重新运行时)

        所以主代码应该看起来像

        stopAllRefresh;  //should instantly(or after completing small chunk) stop refresh
        setTimeout(startRefresh, 100);
        

        并在 setTimeout 中以小块(每个运行

        var curentGraph = 0;
        var curentChunk = 0;
        
        function renderGraphChunk(){
            if (needToBreak)  //check if break rendering
            {exit};
        // Render chunk here
            render(curentGraph, curentChunk);
            curentChunk +=1;
        
            setTimeout(renderGraphChunk, 1);
        }
        

        这只是一个想法草图,实际实现可能完全不同

        【讨论】:

          【解决方案6】:

          如果没有 web worker,你想做的事情就无法完成,这只能在一些最新的浏览器,特别是 Chrome 中实现。

          否则,您必须在队列中中断您的算法。就像 jQuery UI 将下一个动画计算放入队列中一样。 http://api.jquery.com/jQuery.queue/

          这是一个简单的队列,下一个指令集在 setTimeout 的帮助下排队。

           for (i=0; i <1000; i++)
           {
               process (i) ;
           }
          

          可以翻译成

           function queue(s,n, f)
           {
              this.i=s;
              this.n=n;
              this.f=f;
              this.step = function(){
                 if ( this.i <this.n)
                 { 
                      this.f(this.i);
                      this.i = this.i +1;
                      var t = this;
                      setTimeout( function ( ) { t.step(); } , 5);
                 }
              }
              this.step();
          
           }
          
          
           queue ( O, 1000, function(i){
               process(i);
           }) ;
          

          这只是一个示例,说明如何编写同步 for 循环以使用较小的独立迭代异步执行相同的逻辑。

          【讨论】:

          • 问题是当你必须将mousemove事件添加到队列中时(相当于滚动条移动),你不能因为这段代码是在第二次刷新之后执行的(这正是问题所在)。
          • 第二次刷新后不执行,5/1000秒后刷新,甚至可以给1ms作为setTimeout参数。它是即时且非阻塞的。
          • 我不知道该怎么做。如果您以我的 jsfiddle 示例为例,您能否向我提供您的解决方案的更新。因为它,我不知道何时添加到队列中,因为在您的情况下,我需要在事件(在我的示例中为鼠标单击)期间添加到队列中,该事件不会在两次调用复杂算法之间随时发生(这是我的问题)
          • 添加或初始化队列不是问题,它可以随时完成,我告诉你的是你必须将你的复杂算法分成更小的步骤并将操作排队。如果您提供复杂的算法,我只能为您提供帮助,您的 for 语句必须排队到更小的操作中,您的复杂算法必须在 1 毫秒后分解为单个步骤。
          • 好的,我明白你在说什么。问题是我无法更改这个复杂的算法,因为这是一个外部 API(高图表),我不想更改它以保持其清洁以用于更新目的。我已更新 jsfiddle 以反映我无法触摸此代码块
          【解决方案7】:

          尝试查看网络工作者。我认为它在这种情况下会很有用。

          1. http://en.wikipedia.org/wiki/Web_worker

          2. http://www.html5rocks.com/en/tutorials/workers/basics/

          【讨论】:

          • 我不能使用网络工作者,因为我在复杂的算法中使用了 DOM
          猜你喜欢
          • 1970-01-01
          • 2016-07-22
          • 1970-01-01
          • 2014-05-05
          • 2017-10-21
          • 1970-01-01
          • 1970-01-01
          • 2018-05-04
          • 1970-01-01
          相关资源
          最近更新 更多