上下文
我有大约 10 个复杂的图表,每个图表需要 5 秒来刷新。如果我对这 10 个图进行循环,刷新大约需要 50 秒。在这 50 秒内,用户可以移动滚动条。如果移动滚动条,刷新必须停止,当滚动条停止移动时,刷新再次发生。
我在循环中使用 setTimeout 函数让界面刷新。 算法是:
- 渲染第一张图
- setTimeout(渲染第二张图,200)
- 第二张图渲染完成后,在200ms内渲染第三张图,以此类推
setTimeout 允许我们捕获滚动条事件并在下一次刷新时清除超时以避免在移动滚动条之前等待 50 秒...
问题是它不会随时运行。
采用以下简单代码(您可以在这个 fiddle 中尝试: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 毫秒计时器和复杂的接口(interface)上)。请不要提供“优化图形渲染”这样的解决方案,这不是这里的问题。
编辑:cernunnos 对问题有更好的解释: 此外,通过“阻塞”循环中的进程,您可以确保在该循环完成之前无法处理任何事件,因此任何事件都只会在每个循环执行之间处理(并清除超时)(因此为什么您有时必须等待 2 次或更多次完整执行才能中断)。
问题恰好包含在粗体字中:我想确保在我想要的时候中断执行,而不是在中断之前等待 2 次或更多次完整执行
第二次编辑:
总结:使用这个 jsfiddle:http://jsfiddle.net/BwNca/5/ (上面的代码)。
更新此 jsfiddle 并提供解决方案:
鼠标在红色 div 上移动。然后点击并继续移动:右边的计数器只能上升一次。但有时它会在第一个计数器再次运行之前加注 2 或 3 次...这就是问题所在,它只能加注一次!
最佳答案
这里的大问题是 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/
关于Javascript : setTimeout and interface freezing,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15973089/