Why is setTimeout(fn, 0) sometimes useful?
我最近遇到了一个相当讨厌的bug,其中代码通过javascript动态加载了一个
1 | field.selectedIndex = element.index; |
但是,此代码不起作用。即使字段的
1 2 3 4 5 6 7 8 9 | var wrapFn = (function() { var myField = field; var myElement = element; return function() { myField.selectedIndex = myElement.index; } })(); setTimeout(wrapFn, 0); |
这是有效的!
我有一个解决我问题的办法,但我很不安,我不知道为什么这能解决我的问题。有人有官方解释吗?使用
这是因为你在做合作的多任务。
一个浏览器必须同时做很多事情,其中之一就是执行javascript。但javascript经常使用的一个功能是要求浏览器构建一个显示元素。这通常被认为是同步完成的(特别是当javascript不是并行执行时),但不能保证是这样,而且javascript没有一个定义良好的等待机制。
解决方案是"暂停"JavaScript的执行,让渲染线程跟上进度。这就是超时为0的
(实际上,
IE6恰好更容易出现这种错误,但我已经看到它出现在老版本的Mozilla和Firefox中。
参见PhilipRoberts的演讲"事件循环到底是什么?"更详细的解释。
前言:好的。
重要提示:虽然大多数人都赞成并接受了,@staticsan接受的答案实际上是不正确的!-请参阅David Mulder的评论了解原因。好的。
其他一些答案是正确的,但实际上并没有说明要解决的问题是什么,所以我创建了这个答案来展示详细的说明。好的。
因此,我发布了一篇详细的浏览文章,介绍了浏览器的功能以及使用
更新:我做了一个jsiddle来演示下面的解释:http://jsiddle.net/c2ybe/31/。感谢@thangchang帮助启动它。好的。
更新2:为了防止JSfiddle网站死机,或者删除代码,我在最后将代码添加到这个答案中。好的。
细节:好的。
想象一个带有"做某事"按钮和结果div的Web应用程序。好的。
EDOCX1的"1"处理程序为"do-Objt"按钮调用一个函数"LangCalc()",它执行2件事:好的。
做一个很长的计算(假设需要3分钟)好的。
将计算结果打印到结果部分。好的。
现在,你的用户开始测试这个,点击"做点什么"按钮,页面坐在那里,似乎3分钟没有做任何事情,他们变得焦躁不安,再次点击按钮,等待1分钟,什么都没有发生,再次点击按钮…好的。
问题是显而易见的——你需要一个"状态"分区,它显示了正在发生的事情。让我们看看这是如何工作的。好的。
因此,添加一个"status"DIV(最初为空),并修改
填充状态"正在计算…可能需要3分钟进入状态分区好的。
做一个很长的计算(假设需要3分钟)好的。
将计算结果打印到结果部分。好的。
将状态"Calculation Done"填充到状态DIV中好的。
而且,你很高兴地将这个应用程序交给用户重新测试。好的。
他们回来时看起来很生气。并解释当他们点击按钮时,状态DIV从未更新为"计算…"状态!!!!好的。
你挠头,在StackOverflow上四处打听(或者阅读文档或谷歌),然后意识到问题:好的。
浏览器将事件产生的所有"todo"任务(包括ui任务和javascript命令)放在一个队列中。不幸的是,用新的"calculating…"值重新绘制"status"DIV是一个单独的TODO,它将转到队列的末尾!好的。
下面是用户测试期间的事件细分,每个事件后的队列内容:好的。
- 队列:
[Empty] 。 - 事件:点击按钮。事件后排队:
[Execute OnClick handler(lines 1-4)] 。 - 事件:执行onclick处理程序中的第一行(例如change status div value)。事件后排队:
[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] 。 - 事件:在处理程序中执行第三行(填充结果DIV)。后排队:
[Execute OnClick handler(line 4), re-draw Status DIV with"Calculating" value, re-draw result DIV with result] 。 - 事件:在处理程序中执行第4行(用"done"填充status 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 handler sub执行隐含的return ,将"execute onclick handler"从队列中取出,开始执行队列中的下一项。 - 注意:由于我们已经完成了计算,用户已经过了3分钟。重新绘制事件还没有发生!!!!
- 事件:用"计算"值重新绘制状态DIV。我们重新抽签,然后把它从队列中去掉。
- 事件:用结果值重新绘制结果DIV。我们重新抽签,然后把它从队列中去掉。
- 事件:用"完成"值重新绘制状态DIV。我们重新抽签,然后把它从队列中去掉。敏锐的观察者甚至可能会注意到"status div"的"calculating"值在计算完成后会闪烁几微秒。
因此,潜在的问题是"状态"div的重绘事件被放置在队列的末尾,在"执行行2"事件之后需要3分钟,所以实际重画直到计算完成后才发生。好的。
救援工作是EDCOX1×8。它有什么帮助?因为通过EDCOX1(9)来调用长执行代码,实际上创建了2个事件:EDCOX1×9执行本身,(由于0超时),正在执行的代码的单独队列条目。好的。
因此,为了解决问题,您将EDCOX1的7位处理程序修改为两个语句(在一个新函数中或仅在EDCOX1(7)中的一个块中):好的。
填充状态"正在计算…可能需要3分钟进入状态分区好的。
执行
那么,现在事件序列和队列是什么样子的?好的。
- 队列:
[Empty] 。 - 事件:点击按钮。事件后排队:
[Execute OnClick handler(status update, setTimeout() call)] 。 - 事件:执行onclick处理程序中的第一行(例如change status div value)。事件后排队:
[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)] 。请注意,这个重绘事件可能会在警报响起之前发生,这也同样有效。 - …
万岁!在开始计算之前,status div刚刚更新为"calculating…"!!!!好的。
下面是jfiddle的示例代码,说明了这些示例:http://jsfiddle.net/c2ybe/31/:好的。
HTML代码:好的。
1 2 3 4 5 6 7 8 | <table border=1> <tr><td><button id='do'>Do long calc - bad status!</button></td> <td>Not Calculating yet.</td> </tr> <tr><td><button id='do_ok'>Do long calc - good status!</button></td> <td>Not Calculating yet.</td> </tr> </table> |
javascript代码:(在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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); }); |
好啊。
看看JohnResig关于JavaScript计时器如何工作的文章。设置超时时,它实际上会将异步代码排队,直到引擎执行当前的调用堆栈。
大多数浏览器都有一个称为主线程的进程,该进程负责执行一些JavaScript任务、UI更新,例如:绘制、重画或回流等。
一些JavaScript执行和UI更新任务排队到浏览器消息队列,然后被发送到要执行的浏览器主线程。
当主线程繁忙时生成UI更新时,任务将添加到消息队列中。
检查:设置超时
这里有相互冲突的赞成的答案,没有证据就没有办法知道该相信谁。这里有证据表明@dvk是正确的,@salvadordali是错误的。后者声称:
"And here is why: it is not possible to have setTimeout with a time
delay of 0 milliseconds. The Minimum value is determined by the
browser and it is not 0 milliseconds. Historically browsers sets this
minimum to 10 milliseconds, but the HTML5 specs and modern browsers
have it set at 4 milliseconds."
4ms最小超时与正在发生的事情无关。实际发生的是,setTimeout将回调函数推送到执行队列的末尾。如果在setTimeout(callback,0)之后有运行几秒钟的阻塞代码,则在阻塞代码完成之前,将不会在几秒钟内执行回调。试试这个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 |
输出为:
1 2 3 4 | setting timeout 0 callback at 0 starting blocking loop at 5 blocking loop ended at 3000 in timeout callback at 3033 |
这样做的一个原因是将代码的执行延迟到单独的后续事件循环。当响应某种类型的浏览器事件(例如鼠标单击)时,有时只有在处理当前事件之后才需要执行操作。
现在是2015年了,我应该注意还有
这是一个有着古老答案的老问题。我想重新审视这个问题,并回答为什么会发生这种情况,而不是为什么这种情况有用。
所以你有两个功能:
1 2 3 4 5 6 7 8 9 | var f1 = function () { setTimeout(function(){ console.log("f1","First function call..."); }, 0); }; var f2 = function () { console.log("f2","Second call..."); }; |
然后按以下顺序调用
这就是为什么:不可能让
If nesting level is greater than 5, and timeout is less than 4, then
increase timeout to 4.
同样来自Mozilla:
To implement a 0 ms timeout in a modern browser, you can use
window.postMessage() as described here.
阅读以下文章后获取P.S.信息。
由于它的持续时间为
这两个评价最高的答案都是错误的。查看并发模型和事件循环上的MDN描述,应该可以清楚地看到发生了什么(MDN资源是一个真正的gem)。而且,除了"解决"这个小问题之外,简单地使用
这里实际发生的并不是"由于并发性,浏览器可能还没有准备好",或者基于"每行都是一个添加到队列后面的事件"的内容。好的。
DVK提供的JSFIDLE确实说明了一个问题,但他对此的解释并不正确。好的。
在他的代码中发生的事情是,他首先在EDCOX1"2"按钮上将事件处理程序附加到EDCOX1的1事件。好的。
然后,当您实际单击该按钮时,将创建一个引用事件处理程序函数的
这就是它变得有趣的地方。我们已经习惯了把javascript看作是异步的,以至于我们很容易忽略这个小事实:在执行下一个帧之前,必须完全执行任何帧。没有并发性,伙计们。好的。
这是什么意思?这意味着无论何时从消息队列调用函数,它都会阻塞队列,直到它生成的堆栈被清空。或者,更一般地说,它会一直阻塞,直到函数返回。它可以阻止所有操作,包括DOM呈现操作、滚动和其他操作。如果需要确认,只需尝试在小提琴中增加长时间运行操作的持续时间(例如,再运行外部循环10次),您会注意到,当它运行时,您无法滚动页面。如果运行时间足够长,浏览器会询问您是否要终止进程,因为这会使页面没有响应。正在执行帧,事件循环和消息队列将一直保持到完成为止。好的。
那么,为什么这篇文章的副作用没有更新呢?因为当您更改了dom中元素的值时-您可以在更改后立即对其值执行
这是因为我们实际上在等待代码完成运行。我们没有说"有人获取这个,然后用结果调用这个函数,谢谢,现在我已经完成了imma返回,现在做任何事情",就像我们通常对基于事件的异步javascript所做的那样。我们输入一个click事件处理函数,更新一个dom元素,调用另一个函数,另一个函数工作很长时间后返回,然后更新同一个dom元素,然后从初始函数返回,有效地清空堆栈。然后浏览器就可以到达队列中的下一条消息,这很可能是我们通过触发一些内部的"on dom mutation"类型事件生成的消息。好的。
在当前正在执行的框架完成(函数已返回)之前,浏览器用户界面无法(或选择不)更新用户界面。就个人而言,我认为这是设计而非限制。好的。
那么为什么
注意,a)长时间运行的函数在运行时仍然会阻塞所有内容,b)您不能保证UI更新实际上在消息队列中处于领先地位。在我2018年6月的Chrome浏览器上,
好吧,那么使用
首先,在这样的事件处理程序上使用
一位同事在错误理解事件循环时,试图通过让一些模板呈现代码使用
第一个问题是显而易见的;您不能线程化Javascript,所以在添加模糊的时候,这里什么也得不到。其次,您现在已经有效地将模板的呈现从可能的事件侦听器堆栈中分离出来,这些侦听器可能期望已呈现模板,但很可能尚未呈现模板。该函数的实际行为现在是非确定性的,正如任何运行它或依赖它的函数一样(在不知情的情况下)。你可以做出有根据的猜测,但是你不能正确地为它的行为编码。好的。
在编写依赖于其逻辑的新事件处理程序时,"修复"是也使用
但是我们能做什么呢?好吧,正如引用的MDN文章所建议的那样,要么将工作拆分为多条消息(如果可以的话),以便排队的其他消息可以与您的工作交错并在运行时执行,要么使用Web工作者,它可以与您的页面一起运行,并在完成计算后返回结果。好的。
哦,如果你在想,"好吧,难道我不能在长时间运行的函数中放一个回调,使其异步吗?,那么不是。回调不会使它成为异步的,在显式调用回调之前,它仍然需要运行长时间运行的代码。好的。好啊。
另一件事是将函数调用推到堆栈的底部,防止递归调用函数时堆栈溢出。这有一个
关于执行循环和在其他代码完成之前呈现DOM的答案是正确的。javascript中的零秒超时有助于使代码伪多线程,即使它不是。
我想补充一点,在javascript中,跨浏览器/跨平台零秒超时的最佳值实际上是20毫秒而不是0(零),因为许多移动浏览器由于AMD芯片的时钟限制无法注册小于20毫秒的超时。
此外,不涉及DOM操作的长时间运行的进程现在应该发送给Web工作者,因为他们提供了真正的多线程执行javascript。
问题是您试图对一个不存在的元素执行一个javascript操作。元素尚未加载,
StimTimeOn 0在建立延迟承诺的模式中也是非常有用的,您希望马上返回:
1 2 3 4 5 6 7 8 | 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(); } |
通过调用setTimeout,您可以给页面时间来响应用户正在做的任何事情。这对于在页面加载期间运行的函数特别有用。
其他一些设置超时很有用的情况:
您希望将长时间运行的循环或计算拆分为较小的组件,这样浏览器就不会显示为"冻结"或说"页面上的脚本正忙"。
您希望在单击时禁用表单提交按钮,但如果禁用onclick处理程序中的按钮,则不会提交表单。设置时间为零的技巧是,允许事件结束,表单开始提交,然后您的按钮可以被禁用。
Javascript是单线程应用程序,因此不允许同时运行函数,因此可以使用此事件循环。所以,setTimeout(fn,0)所做的就是将其导入到任务请求中,当调用堆栈为空时执行任务请求。我知道这个解释很无聊,所以我建议你看这段视频,这将帮助你如何在浏览器的引擎盖下工作。看看这个视频:https://www.youtube.com/watch?时间继续=392&v=8aghzqkofbq