9

语境

我有大约 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 次……这就是问题所在,它必须只加注一次!

4

7 回答 7

2

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

于 2013-04-20T16:44:43.767 回答
0

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

于 2013-04-17T01:10:52.097 回答
0

你想要做的事情离不开 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 循环以使用较小的独立迭代异步执行相同的逻辑。

于 2013-04-21T19:09:09.807 回答
0

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

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

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

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

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

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

于 2013-04-12T14:16:18.647 回答
0

任何长时间运行的功能都会占用您的浏览器窗口。考虑使用WebWorkers将您的 complexAlgorithm() 移出您的主要 JavaScript 代码。

于 2013-04-19T14:29:58.400 回答
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);
}

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

于 2013-04-19T17:45:43.743 回答
0

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

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

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

于 2013-04-22T09:51:35.487 回答