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

时间:2009-04-22 21:46:08

标签: javascript dom event-loop

我最近遇到了一个相当讨厌的错误,其中代码是通过JavaScript动态加载<select>的。此动态加载的<select>具有预先选择的值。在IE6中,我们已经有代码来修复所选的<option>,因为有时<select>的{​​{1}}值与选定的selectedIndex {{}}不同步1}}属性,如下所示:

<option>

但是,此代码无效。即使正确设置了字段index,最终也会选择错误的索引。但是,如果我在正确的时间插入了field.selectedIndex = element.index; 语句,则会选择正确的选项。考虑到这可能是某种时间问题,我尝试了一些随机的东西,我之前在代码中看到过:

selectedIndex

这很有效!

我已经找到了解决问题的方法,但是我很不安,因为我不知道为什么这会解决我的问题。有人有官方解释吗?通过使用alert()“稍后”调用我的函数,我可以避免哪些浏览器问题?

18 个答案:

答案 0 :(得分:748)

这是有效的,因为你正在进行合作多任务处理。

浏览器必须同时执行许多操作,其中一个就是执行JavaScript。但JavaScript经常用于的一件事是要求浏览器构建一个显示元素。这通常被认为是同步完成的(特别是当JavaScript不是并行执行时),但不能保证是这种情况,并且JavaScript没有明确定义的等待机制。

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

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

IE6恰好更容易出现此错误,但我已经看到它出现在旧版本的Mozilla和Firefox中。


请参阅Philip Roberts的演讲"What the heck is the event loop?"以获得更全面的解释。

答案 1 :(得分:613)

答案 2 :(得分:86)

看看John Resig关于How JavaScript Timers Work的文章。设置超时时,它实际上会将异步代码排队,直到引擎执行当前的调用堆栈。

答案 3 :(得分:21)

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

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

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

setTimeout(fn, 0);将此fn添加到要执行的队列的末尾。 它会在给定的时间后安排在消息队列中添加的任务。

答案 4 :(得分:20)

setTimeout()会花一些时间来加载DOM元素,即使设置为0也是如此。

检查出来:setTimeout

答案 5 :(得分:18)

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

  

“这就是为什么:时间不可能有setTimeout   延迟0毫秒。最小值由。确定   浏览器,它不是0毫秒。历史上,浏览器就是这样设   至少10毫秒,但HTML5规格和现代浏览器   把它设置为4毫秒。“

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

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

答案 6 :(得分:13)

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

编辑现在2015年我应该注意到requestAnimationFrame()也是setTimeout(fn, 0),但它并不完全相同,但它是足够接近{{1}},值得一提。

答案 7 :(得分:9)

这是旧答案的旧问题。我想对这个问题添加一个新的外观,并回答为什么会发生这种情况,而不是为什么这有用。

所以你有两个功能:

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

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

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

原因如下:setTimeout的时间延迟为0毫秒是不可能的。 最小值由浏览器确定,不是0毫秒。 Historically浏览器将此最小值设置为10毫秒,但HTML5 specs和现代浏览器将其设置为4毫秒。

  

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

同样来自mozilla:

  

要在现代浏览器中实现0毫秒超时,您可以使用   window.postMessage()如here所述。

P.S。阅读以下article后会获取信息。

答案 8 :(得分:8)

由于传递的持续时间为0,我认为这是为了从执行流中删除传递给setTimeout的代码。因此,如果它是一个可能需要一段时间的函数,它将不会阻止后续代码执行。

答案 9 :(得分:6)

这两个评价最高的答案都是错误的。 Check out the MDN description on the concurrency model and the event loop,它应该变得清晰(M​​DN资源是真正的宝石)。 简单地使用 setTimeout可以在代码中添加意外问题,除了&#34;解决&#34;这个小问题。

实际在这里发生的事情不是那个&#34;浏览器可能还没有准备就绪,因为并发性,&#34;或者基于&#34的东西;每一行都是一个被添加到队列后面的事件&#34;。

DVK提供的jsfiddle确实说明了一个问题,但他对此的解释并不正确。

他的代码中发生的事情是,他首先将事件处理程序附加到click按钮上的#do事件。

然后,当您实际单击该按钮时,会创建一个引用事件处理函数的message,该函数将添加到message queue。当event loop到达此消息时,它会在堆栈上创建frame,函数调用jsfiddle中的click事件处理程序。

这就是它变得有趣的地方。我们过去常常认为Javascript是异步的,我们很容易忽略这个微小的事实:任何帧都必须在下一帧执行之前完整执行。没有并发,人。

这是什么意思?这意味着无论何时从消息队列调用函数,它都会阻塞队列,直到它生成的堆栈被清空为止。或者,更一般地说,它会阻塞,直到函数返回。它阻止一切,包括DOM渲染操作,滚动等等。如果你想要确认,只是尝试增加小提琴中长时间运行操作的持续时间(例如,多次运行外循环10),并且你会注意到它在运行时,你无法滚动页面。如果运行时间足够长,您的浏览器会询问您是否要终止该过程,因为它会使页面无响应。正在执行框架,事件循环和消息队列将一直停留,直到完成。

那为什么文本的副作用不会更新?因为当您 更改了DOM中元素的值时 - 您可以在更改后立即console.log()其值并看到已更改(其中)显示为什么DVK的解释不正确) - 浏览器正在等待堆栈耗尽(要返回的on处理函数)并因此完成消息,以便它最终可以获得执行作为对我们的变异操作的反应的运行时添加的消息,以及在UI中反映该变异。

这是因为我们实际上在等待代码完成运行。我们还没有说过#34;某人取了这个,然后用结果调用这个函数,谢谢,现在我已经完成了这样的回复,现在做了什么,&#34;就像我们通常使用基于事件的异步Javascript一样。我们输入一个click事件处理函数,我们更新一个DOM元素,我们调用另一个函数,另一个函数工作很长时间然后返回,然后我们更新相同的DOM元素,然后然后返回从初始函数,有效地清空堆栈。然后然后浏览器可以到达队列中的下一条消息,这可能是我们通过触发一些内部&#34; on-DOM-mutation&#34;生成的消息。类型事件。

在当前正在执行的帧完成(函数已返回)之前,浏览器UI无法(或选择不)更新UI。就个人而言,我认为这是设计而非限制。

为什么setTimeout的东西会起作用?它这样做,因为它有效地从其自己的帧中删除对长时间运行的函数的调用,将其安排在稍后的window上下文中执行,以便它本身可以立即返回并允许消息队列处理其他消息。而这个想法就是UI&#34; on update&#34;在更改DOM中的文本时,我们在Javascript中触发的消息现在位于长时间运行的函数排队的消息之前,因此UI更新在我们阻塞很长时间之前发生。

请注意:a)长时间运行的函数在运行时仍会阻止所有内容,并且b)您无法保证UI更新实际上在消息队列中位于其前面。在我的2018年6月Chrome浏览器上,0的值不会修复&#34;小提琴演示的问题--10确实如此。我实际上对此有点嗤之以鼻,因为我认为UI更新消息应该排在它之前是合乎逻辑的,因为它的触发器是在调度长时间运行的函数运行之前执行的。 #34 ;.但也许在V8引擎中有一些可能会干扰的优化,或者我的理解可能只是缺乏。

好的,那么使用setTimeout的问题是什么?对于这种特殊情况,更好的解决方案是什么?

首先,在这样的任何事件处理程序上使用setTimeout的问题,试图缓解另一个问题,很容易弄乱其他代码。这是我工作中的现实例子:

一位同事,在事件循环中对错误的理解,试图&#34;线程&#34;通过让一些模板呈现代码使用setTimeout 0进行渲染的Javascript。他不再在这里问,但我可以假设也许他插入了计时器来衡量渲染速度(这将是函数的返回即时性),并发现使用这种方法会使得该函数的响应速度非常快

第一个问题很明显;你不能使用javascript,所以你在添加混淆时没有赢得任何东西。其次,您现在已经有效地从可能的事件侦听器堆栈中分离了模板的呈现,这些事件侦听器可能期望已经渲染了非常模板,而它可能很少没有。该函数的实际行为现在是非确定性的,因为 - 在不知情的情况下 - 任何运行它或依赖它的函数。您可以进行有根据的猜测,但无法正确编码其行为。

&#34;修复&#34;在编写依赖于其逻辑的新事件处理程序时,使用setTimeout 0。但是,这不是一个修复,很难理解,调试由这样的代码引起的错误并不好玩。有时甚至没有问题,有时它会一直失败,然后有时它会偶尔起作用和破坏,这取决于平台当前的性能以及当时正在发生的其他事情。这就是为什么我个人会建议不要使用这个黑客(一个hack,我们都应该知道它是),除非你真的知道你在做什么以及后果是什么

可以呢?好吧,正如引用的MDN文章建议的那样,要么将工作分成多个消息(如果可以的话),以便排队的其他消息可以与您的工作交错并在其运行时执行,或者使用可以运行的Web工作者与您的页面串联并在完成计算后返回结果。

哦,如果你在想,&#34;那么,我不能在长时间运行的函数中设置一个回调函数来使它异步?,&#34;那就不要。回调并没有使它异步,它仍然必须在显式调用你的回调之前运行长时间运行的代码。

答案 10 :(得分:3)

另一件事是将函数调用推送到堆栈的底部,如果递归调用函数,则防止堆栈溢出。这具有while循环的效果,但允许JavaScript引擎触发其他异步计时器。

答案 11 :(得分:2)

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

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

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

答案 12 :(得分:1)

通过调用setTimeout,您可以给页面时间以响应用户正在执行的操作。这对于页面加载期间运行的函数特别有用。

答案 13 :(得分:1)

问题是您试图对不存在的元素执行Javascript操作。元素尚未加载,setTimeout()通过以下方式为元素加载提供了更多时间:

  1. setTimeout()导致事件为异步,因此在所有同步代码之后执行该事件,从而使您的元素有更多的加载时间。异步回调(如setTimeout()中的回调)放置在事件队列中,并在同步代码堆栈为空之后通过事件循环放入堆栈中。
  2. 在函数setTimeout()中,作为第二个参数的ms的值0通常会稍高一些(4-10ms,具体取决于浏览器)。执行setTimeout()回调所需的时间稍长,这是由事件循环的“滴答声”(如果堆栈为空,则滴答声将堆栈中的回调推入堆栈)引起的。由于性能和电池寿命的原因,事件循环中的滴答声数量被限制为每秒小于或等于1000次的特定数量。

答案 14 :(得分:1)

setTimout on 0在设置延迟保证的模式中也非常有用,您希望立即返回:

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

答案 15 :(得分:1)

其他一些setTimeout很有用的情况:

您希望将长时间运行的循环或计算分解为更小的组件,以便浏览器看起来不会“冻结”或说“页面上的脚本正忙”。

您希望在单击时禁用表单提交按钮,但如果禁用onClick处理程序中的按钮,则不会提交表单。时间为零的setTimeout可以解决问题,允许事件结束,表单开始提交,然后您的按钮可以被禁用。

答案 16 :(得分:0)

Javascript是单线程应用程序,因此不允许同时运行函数,以便实现此事件循环。所以setTimeout(fn,0)正是这样做的,它被推入任务任务,当你的调用堆栈为空时执行。我知道这个解释很无聊,所以我建议你仔细阅读这个视频,这将有助于你在浏览器中如何工作。 观看此视频: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

答案 17 :(得分:0)

//When need "new a", setTimeout(fn, 0) is useful, when need to wait some action. Example:

var a = function (){console.log('a');};
var b = function(){setTimeout(b, 100);}; //wait some action before override this function

//without setTimeout:
console.log('no setTimeout: b.toString():', b.toString());
b();    //"b" is an old function
console.log('no setTieout: a.toString(): ', a.toString());
a();    //and "a" is not overrided

setTimeout(//but with setTimeout(fn, 0):
    function(){
        console.log('After timeout 0, b.toString(): ', b.toString());
        b();    //"b" is a new function
        console.log('After timeout 0, a.toString(): ', a.toString());
        a();    //and "a" is overrided
    },
    0
);

//override var "b", which was been undefined
b = function (){
    a = function(){console.log('new a');};
}