Javascript:setTimeout和界面冻结

时间:2013-04-12 13:50:07

标签: javascript settimeout

上下文

我有大约10个复杂的图表,每个图表需要5秒才能刷新。如果我在这10个图表上进行循环,则刷新大约需要50秒。在这50秒期间,用户可以移动滚动条。如果移动滚动条,则必须停止刷新,当滚动条停止移动时,再次进行刷新。

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

  • 渲染第一张图
  • setTimeout(渲染第二张图,200)
  • 渲染第二个图形时,在200ms内渲染第三个图形,依此类推

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事件的代码(因此它将取消超时和冻结)但有时点击计数器会获得2点或更多点,而不是因为mouvemove事件只获得1点......

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

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

如何确保在鼠标移动时中断超时?

PS:在下面的简单示例中,如果您使用200ms更新超时,则所有运行都很完美,但这不是一个可接受的解决方案(真正的问题更复杂,200ms计时器和复杂接口会出现问题)。请不要提供“优化图形渲染”的解决方案,这不是问题所在。

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

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


第二次编辑:

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

更新此jsfiddle并提供解决方案:

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

7 个答案:

答案 0 :(得分:1)

这里的BIG问题是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/

答案 1 :(得分:0)

我可能已经理解了这个问题但是假设你试图在点击一下后阻止界面至少1秒并通过移动鼠标解锁它(在最小1秒之后):

这不是一个很好的睡眠实现,因为你一直在保持进程运行(什么也不做!=休眠),这会浪费资源。

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

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

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

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

答案 2 :(得分:0)

令人惊讶的是,当你setTimeout()时,你还没想出来;你可以在那之后输入支票。变量为true然后废弃等待,或者将其废弃。现在有一种方法可以检查滚动条滚动。在使用平均值在变量内检查它之后,您将发现这将在滚动条形图时重复无效次数,使许多执行时间为5秒。要解决此问题,请添加1秒等待,以确保它不会重复。您的欢迎:))

答案 3 :(得分:0)

任何长时间运行的功能都会占用您的浏览器窗口。考虑使用WebWorkers在主javascript代码之外移动complexAlgorithm()。

答案 4 :(得分:0)

答案在你的问题中

  

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

你应该以这样的方式编写complexAlgorithm,你几乎可以在中间立即制动它(当你知道你必须重新运行时)

所以主要代码看起来应该像

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

并在setTimeout中以小块(每次运行<1秒)渲染图形 像

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);
}

这只是一个想法草图,真正的实现可以完全不同

答案 5 :(得分:0)

如果没有网络工作人员,您想要做的事情是无法完成的,这只是在某些最新的浏览器中实现的,特别是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循环来执行相同的逻辑。

答案 6 :(得分:0)

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

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

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