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

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

field.selectedIndex = element.index;

但是,这段代码不起作用。即使字段的selectedIndex设置正确,最终也会选择错误的索引。然而,如果我在正确的时间插入alert()语句,就会选择正确的选项。认为这可能是某种时间问题,我随机尝试了一些我以前在代码中看到的东西:

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

这招奏效了!

我有一个解决方案来解决我的问题,但是我很不安,我不知道为什么这可以修复我的问题。有人有官方的解释吗?使用setTimeout()调用我的函数“以后”来避免什么浏览器问题?

356066 次浏览

setTimeout()为您争取了一些时间,直到加载DOM元素,即使设置为0。

看看这个:设置超时

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

在问题中,存在竞争条件之间:

  1. 浏览器初始化下拉列表的尝试,准备更新其选定的索引,以及
  2. 设置所选索引的代码

您的代码一直在赢得这场比赛,并试图在浏览器准备就绪之前设置下拉选择,这意味着会出现bug。

这种竞争之所以存在,是因为JavaScript有一个与页面呈现共享的单线程执行。实际上,运行JavaScript会阻止DOM的更新。

您的解决方法是:

setTimeout(callback, 0)

使用回调调用setTimeout,并将零作为第二个参数,将安排回调在最短的延迟之后运行异步-当选项卡有焦点并且JavaScript执行线程不忙时,这将是大约10毫秒。

因此,OP的解决方案是将选定索引的设置延迟大约10ms。这使浏览器有机会初始化DOM,修复bug。

每个版本的Internet Explorer都表现出古怪的行为,这种解决方法有时是必要的。或者它可能是OP代码库中的真正bug。


请参阅Philip Roberts演讲“什么是事件循环?”以获得更全面的解释。

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

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

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

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

前言:

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

因此,我发布了一个详细介绍浏览器的作用以及使用#0的帮助。它看起来很长,但实际上非常简单明了-我只是把它做得非常详细。

更新:我做了一个JSFiddle来现场演示下面的解释:http://jsfiddle.net/C2YBE/31/。许多感谢给@Thangong帮助启动它。

更新2:以防JSFiddle网站死亡或删除代码,我在最后将代码添加到这个答案中。


细节

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

“做某事”按钮的onClick处理程序调用一个函数“LongCalc()”,它做了两件事:

  1. 做一个很长的计算(说需要3分钟)

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

现在,你的用户开始测试这个,点击“做点什么”按钮,页面坐在那里似乎什么也没做3分钟,他们变得焦躁不安,再次点击按钮,等待1分钟,什么都没发生,再次点击按钮……

问题是显而易见的--你想要一个“状态”DIV,它显示正在发生的事情。


因此,您添加了一个“状态”DIV(最初为空),并修改onclick处理程序(函数LongCalc())以执行4件事:

  1. 在状态DIV中填充状态“计算…可能需要约3分钟”

  2. 做一个很长的计算(说需要3分钟)

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

  4. 将状态“计算完成”填充到状态DIV中

而且,您很乐意将应用程序提供给用户重新测试。

他们回来看你很生气。并解释说,当他们点击按钮时,状态DIV从未更新为“计算…”状态!!!


你挠头,在StackOverflow上四处询问(或阅读文档或谷歌),并意识到问题:

浏览器将事件产生的所有“待办事项”任务(UI任务和JavaScript命令)放入单队列。不幸的是,使用新的“计算…”值重新绘制“状态”DIV是一个单独的待办事项,它将进入队列的末尾!

以下是用户测试期间事件的细分,每个事件后队列的内容:

  • 队列:[Empty]
  • 事件:点击按钮。事件后队列:[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行(用“DONE”填充状态DIV)。队列:[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。我们进行重新绘制并将其从队列中删除。
  • 事件:使用结果值重新绘制Result DIV。我们进行重新绘制并将其从队列中删除。
  • 事件:使用“完成”值重新绘制状态DIV。我们进行重新绘制并将其从队列中删除。眼尖的观众甚至可能会注意到“状态DIV与“计算”值闪烁几微秒-在计算完成后

因此,潜在的问题是“状态”DIV的重新绘制事件被放置在最后的队列中,在“执行第2行”事件之后需要3分钟,因此实际的重新绘制直到计算完成后才会发生。


救援来了setTimeout()。它有什么帮助?因为通过setTimeout调用长时间执行的代码,你实际上创建了2个事件:setTimeout执行本身,以及(由于0超时),正在执行的代码的单独队列条目。

因此,为了解决您的问题,您将onClick处理程序修改为TWO语句(在新函数中或仅在onClick中的块中):

  1. 在状态DIV中填充状态“计算…可能需要约3分钟”

  2. 以0超时执行#0并调用#1函数

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

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

  • 队列:[Empty]
  • 事件:点击按钮。事件后队列:[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/

超文本标记语言代码:

<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 500window.setTimeout(function (){ long_running('#status_ok') }, 0);});

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

setTimeout有用的其他一些情况:

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

您想在单击时禁用表单提交按钮,但是如果您在onClick处理程序中禁用该按钮,表单将不会被提交。setTimeout的时间为零就可以了,允许事件结束,表单开始提交,然后您的按钮可以被禁用。

浏览器有一个称为“主线程”的进程,负责执行一些JavaScript任务,UI更新,例如:绘画、重画、重排等。JavaScript任务被排队到消息队列,然后被分派到浏览器的主线程执行。当主线程繁忙时生成UI更新时,任务会添加到消息队列中。

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

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

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

这是一个老问题,老答案。我想对这个问题有一个新的看法,并回答为什么会发生这种情况,而不是为什么这很有用。

所以你有两个功能:

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毫秒。历史上浏览器将此最小值设置为10毫秒,但HTML5规范和现代浏览器将其设置为4毫秒。

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

也来自mozilla:

要在现代浏览器中实现0 ms超时,您可以使用window.post消息()描述这里.

P. S.阅读以下文章后获取信息。

这里有相互矛盾的投票答案,没有证据就没有办法知道该相信谁。这里有证据证明@DVK是正确的,@萨尔瓦多达利是不正确的。后者声称:

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

4ms的最小超时与正在发生的事情无关。真正发生的是setTimeout将回调函数推送到执行队列的末尾。如果在setTimeout(call back,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 belowfunction sinceStart () {return new Date().getTime() - startTime} // sinceStart} // testSettimeout0

输出为:

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

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

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

Javascript是单线程应用程序,因此不允许并发运行函数,因此要实现此事件循环是使用的。所以setTimeout(fn,0)到底做了什么,它被推到任务任务中,当你的调用堆栈为空时执行。我知道这个解释很无聊,所以我建议你看一下这个视频,这将帮助你在浏览器中如何工作。看看这个视频:-https://www.youtube.com/watch?time_continue=392&;v=8aGhZQkoFbQ

查看关于并发模型和事件循环的MDN描述,应该清楚发生了什么(MDN资源是一个真正的宝石)。简单地使用setTimeout除了“解决”这个小问题之外,还可以在你的代码中添加意想不到的问题。

这里的实际上并不是说“浏览器可能还没有准备好,因为并发”,或者基于“每行都是一个添加到队列后面的事件”。

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

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

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

这就是它变得有趣的地方。我们习惯于将Javascript视为异步的,以至于我们容易忽略这个小事实:在执行下一帧之前,必须完全执行任何帧。没有并发,人。

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

那么为什么文本没有更新呢?因为当您更改DOM中元素的值时-您可以在更改后立即console.log()其值并看到它已经已更改(这表明为什么DVK的解释是不正确的)-浏览器正在等待堆栈耗尽(on处理程序函数返回),从而完成消息,以便它最终可以执行运行时添加的消息,作为对我们突变操作的反应,并在UI中反映该突变。

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

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

那么为什么setTimeout的东西起作用呢?它这样做是因为它有效地从自己的框架中删除了对长期运行函数的调用,安排它稍后在window上下文中执行,这样它本身就可以立即返回并允许消息队列处理其他消息。这个想法是,我们在Javascript中更改DOM中的文本时触发的UI“更新”消息现在领先于长期运行函数排队的消息,这样UI更新就会在我们长时间阻塞之前发生。

请注意,a)长时间运行的函数仍然阻止在它运行时一切,b)你不能保证UI更新实际上在消息队列中排在它前面。在我2018年6月Chrome浏览器上,值0并不能“修复”小提琴演示的问题——10可以。我实际上对此有点窒息,因为在我看来,UI更新消息应该在它之前排队是合乎逻辑的,因为它的触发器是在调度长时间运行的函数“稍后”运行之前执行的。但也许V8引擎中的一些优化可能会干扰,或者我只是缺乏理解。

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

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

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

第一个问题很明显;你不能线程化javascript,所以当你添加混淆时,你什么也得不到。其次,你现在已经有效地将模板的呈现从可能的事件侦听器堆栈中分离出来,这些侦听器可能期望该模板已经呈现,而它很可能没有呈现。该函数的实际行为现在是不确定的,任何运行它或依赖它的函数都不知道是这样的。你可以做出有根据的猜测,但你不能为它的行为正确编码。

编写依赖其逻辑的新事件处理程序时的“修复”是使用setTimeout 0。但是,这不是修复,很难理解,并且调试由这样的代码引起的错误也不有趣。有时永远没有问题,有时它偶然失败,然后有时候它会偶尔工作和中断,这取决于平台当前的性能和当时发生的其他任何事情。这就是为什么我个人建议不要使用这种黑客攻击(它是黑客攻击,我们都应该知道它是),除非你真的知道自己在做什么和后果是什么。

那么,正如引用的MDN文章所建议的那样,要么将工作拆分为多个消息(如果可以),以便排队的其他消息可以与您的工作交错并在运行时执行,要么使用Web Worker,它可以与您的页面一起运行并在完成计算后返回结果。

哦,如果你在想,“好吧,我不能在长时间运行的函数中放一个回调来使它成为异步的吗?”,那么没有。回调不会使它成为异步的,它仍然必须在显式调用回调之前运行长时间运行的代码。

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

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

如果你不想看一整段视频,这里有一个简单的解释,一个人需要理解的事情,以便能够理解这个问题的答案:

  1. JavaScript是单线程的意味着它在运行时一次只做一件事。
  2. 但是运行JavaScript的环境可以是多线程的。例如,浏览器通常是多线程的生物,即能够一次做多件事。所以他们可以运行JavaScript,同时也可以跟踪处理其他东西。

从这一点开始,我们谈论的是“浏览器中”的JavaScript。像setTimeout这样的东西确实是浏览器的东西,并不是JavaScript本身的一部分。

  1. 允许JavaScript异步运行的是多线程浏览器!除了JavaScript使用的主要空间(称为的调用堆栈)来放置每一行代码并逐一运行它们之外,浏览器还为JavaScript提供了另一个空间来放置东西。

现在我们把这个空间称为第二空间

  1. 让我们假设fn是一个函数。这里要理解的重要一点是#1调用不等于#2调用将在下面进一步解释。

不是0延迟,让我们先假设另一个延迟,例如5000毫秒:setTimeout(fn, 5000);。重要的是要注意,这仍然是一个“函数调用”,所以它必须放在主空间上,完成后从它中删除,但是等等!,我们不喜欢整个冗长而无聊的5秒延迟。这将阻塞主空间,并且不允许JavaScript在此期间运行任何其他东西。

谢天谢地,这不是浏览器设计者设计它们的工作方式。相反,这个调用(#0)是立即完成的。这非常重要:即使有5000毫秒的延迟,这个函数调用也会在瞬间完成!接下来会发生什么?它被从主空间中删除。它将放在哪里?(因为我们不想失去它)。你可能猜对了:浏览器听到调用这个并将其放在第二个空间。输入图片描述

浏览器跟踪5秒延迟一旦它通过,它会查看主空间,然后“WHEN IT'S EMPTY”,将#0调用放回它。这就是setTimeout的工作方式。

所以,回到setTimeout(fn, 0),即使延迟为零,这仍然是对浏览器的调用,浏览器立即听到并拾取它,并将其放在第二个空间并将其放回主空间只有当主空间再次是空的,而不是真正的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 functionconsole.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 functionconsole.log('After timeout 0, a.toString(): ', a.toString());a();    //and "a" is overrided},0);
//override var "b", which was been undefinedb = function (){a = function(){console.log('new a');};}