为什么setTimeout(fn,0)有时有用?

我最近遇到了一个相当讨厌的bug,其中代码是通过JavaScriptdynamic加载<select> 。 这个dynamic加载的<select>有一个预先选定的值。 在IE6中,我们已经有了修改所选的<option>代码,因为有时候<select>selectedIndex值将与select的<option>index属性不同步,如下所示:

 field.selectedIndex = element.index; 

但是,这个代码不起作用。 即使字段的selectedIndex设置正确,错误的索引也会被选中。 但是,如果我在正确的时间插入alert()语句,则会select正确的选项。 考虑到这可能是一些时间问题,我尝试了一些随机代码,之前我曾经看过代码:

 var wrapFn = (function() { var myField = field; var myElement = element; return function() { myField.selectedIndex = myElement.index; } })(); setTimeout(wrapFn, 0); 

这工作!

对于我的问题,我有一个解决scheme,但是我不确定为什么这会解决我的问题。 有没有人有正式的解释? 什么浏览器问题,我通过使用setTimeout()调用我的函数“以后”避免?

这是有效的,因为你在做合作多任务。

浏览器必须一次完成许多事情,其中​​只有一个是执行JavaScript。 但是JavaScript经常被用来做的事情之一是要求浏览器构build一个显示元素。 这经常被认为是同步完成的(特别是JavaScript不是并行执行的),但是不能保证是这种情况,JavaScript没有一个明确的等待机制。

解决scheme是“暂停”JavaScript执行,以使渲染线程赶上。 这是setTimeout()的超时时间为0的效果。 这就像是C语言中的一个线程/进程产量。虽然它似乎说“立即运行”,但它实际上使浏览器有机会完成一些非JavaScript事情,这些事情在参加这个新的JavaScript之前就已经等待完成了。

(实际上, setTimeout()在执行队列的末尾重新排队新的JavaScript,请参阅注释以获得更长的解释。)

IE6碰巧更容易出现这个错误,但我已经看到它发生在旧版本的Mozilla和Firefox上。

前言:

重要提示:虽然这是最有利的和被接受的,@staticsan接受的答案实际上是不正确的! – 请参阅David Mulder的解释原因。

其他一些答案是正确的,但实际上并没有说明问题的解决方法是什么,所以我创build了这个答案来提供详细的说明。

因此,我发布了一个浏览器的详细步骤,以及如何使用setTimeout()帮助 。 它看起来很长,但实际上非常简单直接 – 我只是把它做得非常详细。

更新:我做了一个JSFiddle生活 – 演示下面的解释: http : //jsfiddle.net/C2YBE/31/ 。 非常感谢 @ TengChung帮助启动它。

更新2:万一JSFiddle网站死亡,或删除代码,我最后添加到这个答案的代码。


详情

想象一个带有“做某事”button和结果div的web应用程序。

“do something”button的onClick处理程序调用一个函数“LongCalc()”,它有两个作用:

  1. 做一个很长的计算(比如花3分钟)

  2. 将计算结果打印到结果div中。

现在,你的用户开始testing这个,点击“做某事”button,页面坐在那里做3分钟看似没有,他们不安,再次单击button,等待1分钟,没有任何反应,再次单击button…

问题是显而易见的 – 你想要一个“状态”DIV,它显示了正在发生的事情。 让我们看看这是如何工作的。


所以你添加一个“状态”DIV(最初是空的),并修改onclick处理函数(函数LongCalc() )做4件事情:

  1. 将状态“计算…可能需要〜3分钟”填入状态DIV

  2. 做一个很长的计算(比如花3分钟)

  3. 将计算结果打印到结果div中。

  4. 将状态“计算完成”填入状态DIV

而且,你高兴地把应用程序给用户重新testing。

他们看起来很生气。 并解释说,当他们点击button, 状态DIV从来没有更新与“计算…”状态!


你抓住你的头,问在StackOverflow(或阅读文档或谷歌)周围,并意识到这个问题:

浏览器将所有由事件产生的“TODO”任务(UI任务和JavaScript命令)放到一个队列中 。 不幸的是,用新的“Calculating …”值重新绘制“状态”DIV是一个单独的TODO,它会排到最后!

以下是用户testing期间的事件细目,每个事件之后的队列内容:

  • 队列: [Empty]
  • 事件:点击button。 事件后排队: [Execute OnClick handler(lines 1-4)]
  • 事件:在OnClick处理程序中执行第一行(例如,更改状态DIV值)。 事件之后的队列: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]请注意,当DOM改变瞬间发生时,为了重新绘制相应的DOM元素,您需要一个由DOM更改触发的新事件,该事件位于队列末尾
  • 问题!!! 问题!!! 详情如下。
  • 事件:执行处理程序(计算)中的第二行。 排队后: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • 事件:执行处理程序中的第3行(填充结果DIV)。 排队后: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
  • 事件:执行处理程序中的第4行(用“完成”填充状态DIV)。 队列: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • 事件:从onclick处理器子执行隐式return 。 我们将“Execute OnClick处理程序”从队列中取出,并开始执行队列中的下一个项目。
  • 注意:由于我们已经完成计算,所以用户已经过了3分钟。 重新绘制事件没有发生!
  • 事件:用“计算”​​值重新绘制状态DIV。 我们做了重新抽签,并把它从队列中取出。
  • 事件:用结果值重新绘制结果DIV。 我们做了重新抽签,并把它从队列中取出。
  • 事件:用“完成”值重新绘制状态DIV。 我们做了重新抽签,并把它从队列中取出。 尖锐的观众甚至可能会注意到“状态DIV与”计算“值闪烁微秒 – 在计算完成后

所以,底层的问题是,“状态”DIV的重绘事件被放置在队列末尾,在“执行线2”事件之后需要3分钟,因此实际的重新绘制不会发生,直到计算完成后。


setTimeout()来解救。 它如何帮助? 因为通过setTimeout调用长执行代码,你实际上创build了两个事件: setTimeout执行本身,和(由于0超时),为正在执行的代码单独的队列条目。

所以,为了解决你的问题,你修改你的onClick处理程序是两个语句(在一个新的function或只是在onClick块):

  1. 将状态“计算…可能需要〜3分钟”填入状态DIV

  2. 用0超时执行setTimeout()并调用LongCalc()函数

    LongCalc()函数几乎与上次相同,但显然没有“计算…”状态DIV更新作为第一步; 而是马上开始计算。

那么,事件序列和队列现在是什么样的呢?

  • 队列: [Empty]
  • 事件:点击button。 事件之后的队列: [Execute OnClick handler(status update, setTimeout() call)]
  • 事件:在OnClick处理程序中执行第一行(例如,更改状态DIV值)。 事件之后排队: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
  • 事件:在处理程序中执行第二行(setTimeout调用)。 排队后: [re-draw Status DIV with "Calculating" value] 。 队列中没有任何新东西再多出0秒。
  • 事件:超时警报在0秒后熄灭。 排队后: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
  • 事件: 用“计算”​​值重新绘制状态DIV 。 排队后: [execute LongCalc (lines 1-3)] 。 请注意,这个重新绘制事件实际上可能发生在闹钟响起之前,这也是一样的。

万岁! 状态DIV在计算开始之前刚刚更新为“正在计算…”!



下面是来自JSFiddle的示例代码,演示了这些示例: http : //jsfiddle.net/C2YBE/31/ :

HTML代码:

 <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td><div id='status'>Not Calculating yet.</div></td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td><div id='status_ok'>Not Calculating yet.</div></td> </tr> </table> 

JavaScript代码:(在onDomReady执行,可能需要jQuery 1.9)

 function long_running(status_div) { var result = 0; // Use 1000/700/300 limits in Chrome, // 300/100/100 in IE8, // 1000/500/200 in FireFox // I have no idea why identical runtimes fail on diff browsers. for (var i = 0; i < 1000; i++) { for (var j = 0; j < 700; j++) { for (var k = 0; k < 300; k++) { result = result + i + j + k; } } } $(status_div).text('calculation done'); } // Assign events to buttons $('#do').on('click', function () { $('#status').text('calculating....'); long_running('#status'); }); $('#do_ok').on('click', function () { $('#status_ok').text('calculating....'); // This works on IE8. Works in Chrome // Does NOT work in FireFox 25 with timeout =0 or =1 // DOES work in FF if you change timeout from 0 to 500 window.setTimeout(function (){ long_running('#status_ok') }, 0); }); 

看看John Resig关于JavaScript定时器如何工作的文章。 当你设置超时时,它实际上将asynchronous代码排队,直到引擎执行当前的调用栈。

大多数浏览器都有一个称为主线程的进程,负责执行一些JavaScript任务,UI更新(例如绘图,重画或回stream等)。

一些JavaScript执行和UI更新任务排队到浏览器消息队列,然后被分派到浏览器主线程执行。

当主线程繁忙时生成UI更新时,任务将被添加到消息队列中。

setTimeout(fn, 0); 将此fn添加到要执行的队列末尾。 它在给定的时间量之后调度在消息队列上添加的任务。

setTimeout()会花费你一些时间,直到DOM元素被加载,即使设置为0。

看看这个: setTimeout

在这里有相互矛盾的答案,没有证据就没有办法知道谁相信。 这是certificate@DVK是正确的,@SalvadorDali是错误的。 后者声称:

“这就是为什么:setTimeout的延迟时间为0毫秒是不可能的,最小值是由浏览器决定的,它不是0毫秒,历史上浏览器将这个最小值设置为10毫秒,但HTML5规范和现代浏览器将其设置为4毫秒。“

4ms最小超时与所发生的事情无关。 真正发生的事情是setTimeout将callback函数推到执行队列的末尾。 如果在setTimeout(callback,0)后面有阻塞的代码需要几秒钟的运行,callback将不会执行几秒钟,直到阻塞代码完成。 试试这个代码:

 function testSettimeout0 () { var startTime = new Date().getTime() console.log('setting timeout 0 callback at ' +sinceStart()) setTimeout(function(){ console.log('in timeout callback at ' +sinceStart()) }, 0) console.log('starting blocking loop at ' +sinceStart()) while (sinceStart() < 3000) { continue } console.log('blocking loop ended at ' +sinceStart()) return // functions below function sinceStart () { return new Date().getTime() - startTime } // sinceStart } // testSettimeout0 

输出是:

 setting timeout 0 callback at 0 starting blocking loop at 5 blocking loop ended at 3000 in timeout callback at 3033 

这样做的一个原因是将代码的执行推迟到一个单独的,后续的事件循环。 当响应某种浏览器事件(例如,鼠标点击)时,有时只有处理当前事件之后才需要执行操作。 setTimeout()方法是最简单的方法。

现在编辑它是2015年我应该注意,还有requestAnimationFrame() ,这是不完全相同的,但它是足够接近setTimeout(fn, 0) ,这是值得一提的。

这是一个旧的问题与旧的答案。 我想在这个问题上增加新的面貌,并回答为什么发生这种情况,而不是为什么这是有用的。

所以你有两个function:

 var f1 = function () { setTimeout(function(){ console.log("f1", "First function call..."); }, 0); }; var f2 = function () { console.log("f2", "Second call..."); }; 

然后按以下顺序调用它们: f1(); f2(); f1(); f2(); 只是为了看到第二个先执行。

这就是为什么: setTimeout不可能有0毫秒的时间延迟。 最小值由浏览器决定,不是0毫秒。 历史上浏览器将这个最小值设置为10毫秒,但HTML5规范和现代浏览器将其设置为4毫秒。

如果嵌套级别大于5,并且超时小于4,则将超时增加到4。

另外从Mozilla:

要在现代浏览器中实现0 ms超时,可以按照此处所述使用window.postMessage()。

PS资料是在阅读以下文章后采取的。

由于它的传递时间为0 ,我想这是为了从执行stream程中移除传递给setTimeout的代码。 所以如果它是一个可能需要一段时间的函数,它不会阻止后续的代码执行。

另一件事是把函数调用推到栈底,如果你recursion地调用一个函数,防止栈溢出。 这有一个while循环的效果,但让JavaScript引擎激发其他asynchronous计时器。

有关执行循环和在其他代码完成之前呈现DOM的答案是正确的。 JavaScript中的零秒超时有助于使代码为伪multithreading,即使它不是。

我想补充一点,JavaScript中跨浏览器/跨平台零秒超时的最佳值实际上大约是20毫秒而不是0(零),因为许多移动浏览器由于时钟限制无法注册小于20毫秒的超时在AMD芯片上。

此外,不涉及DOM操作的长时间运行的stream程现在应该发送给Web Workers,因为它们提供了JavaScript的真正的multithreading执行。

通过调用setTimeout,您可以让页面的时间对用户正在做的事情做出反应。 这对页面加载期间运行的函数特别有用。

setTimeout有用的其他一些情况:

您想要将长时间运行的循环或计算分解为较小的组件,以便浏览器不会显示为“冻结”或者说“页面上的脚本正忙”。

您希望在点击时禁用表单提交button,但是如果您在onClick处理程序中禁用该button,表单将不会被提交。 setTimeout的时间为零的技巧,允许事件结束,表单开始提交,然后你的button可以被禁用。

0上的setTimout在设置延期承诺的模式中也是非常有用的,您希望立即返回:

 myObject.prototype.myMethodDeferred = function() { var deferredObject = $.Deferred(); var that = this; // Because setTimeout won't work right with this setTimeout(function() { return myMethodActualWork.call(that, deferredObject); }, 0); return deferredObject.promise(); }