为什么 setTimeout(fn, 0) 有时很有用?
我最近遇到了一个相当讨厌的错误,其中代码通过 JavaScript 动态加载 有一个预先选择的值。在 IE6 中,我们已经有了修复所选
的代码,因为有时
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()
“稍后”调用我的函数可以避免什么浏览器问题?
I've recently run into a rather nasty bug, wherein the code was loading a <select>
dynamically via JavaScript. This dynamically loaded <select>
had a pre-selected value. In IE6, we already had code to fix the selected <option>
, because sometimes the <select>
's selectedIndex
value would be out of sync with the selected <option>
's index
attribute, as below:
field.selectedIndex = element.index;
However, this code wasn't working. Even though the field's selectedIndex
was being set correctly, the wrong index would end up being selected. However, if I stuck an alert()
statement in at the right time, the correct option would be selected. Thinking this might be some sort of timing issue, I tried something random that I'd seen in code before:
var wrapFn = (function() {
var myField = field;
var myElement = element;
return function() {
myField.selectedIndex = myElement.index;
}
})();
setTimeout(wrapFn, 0);
And this worked!
I've got a solution for my problem, but I'm uneasy that I don't know exactly why this fixes my problem. Does anyone have an official explanation? What browser issue am I avoiding by calling my function "later" using setTimeout()
?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(19)
这里有相互矛盾的赞成答案,没有证据就无法知道该相信谁。这是@DVK 是正确的而@SalvadorDali 是错误的证据。后者声称:
4 毫秒的最小超时与正在发生的情况无关。真正发生的是 setTimeout 将回调函数推到执行队列的末尾。如果在 setTimeout(callback, 0) 之后有阻塞代码,需要几秒钟才能运行,回调将在几秒钟内不会执行,直到阻塞代码完成尝试以下代码:
输出为:
There are conflicting upvoted answers here, and without proof there is no way to know whom to believe. Here is proof that @DVK is right and @SalvadorDali is incorrect. The latter claims:
The 4ms minimum timeout is irrelevant to what is happening. What really happens is that setTimeout pushes the callback function to the end of the execution queue. If after setTimeout(callback, 0) you have blocking code which takes several seconds to run, the callback will not be executed for several seconds, until the blocking code has finished. Try this code:
Output is:
浏览器有一个称为“主线程”的进程,它负责执行一些 JavaScript 任务、UI 更新,例如:绘画、重绘、回流等。
JavaScript 任务会排队到消息队列中,然后分派到浏览器的主线程中执行。
当主线程繁忙时生成 UI 更新时,任务将添加到消息队列中。
Browsers have a process called "main thread", that is responsible for executing some JavaScript tasks, UI updates e.g.: painting, redraw, reflow, etc.
JavaScript tasks are queued to a message queue and then are dispatched to the browser's main thread to be executed.
When UI updates are generated while the main thread is busy, tasks are added into the message queue.
这两个最高评价的答案都是错误的。 查看 MDN 关于并发模型和事件循环的描述 ,并且应该清楚发生了什么(MDN 资源是真正的宝石)。除了“解决”这个小问题之外,简单地使用
setTimeout
还可能会在代码中添加意想不到的问题。这里实际上发生的事情并不是“浏览器可能还没有完全准备好,因为并发”,或者基于“每一行都是一个被添加到队列后面的事件”。
DVK提供的jsfiddle确实说明了一个问题,但他的解释并不正确。
他的代码中发生的情况是,他首先将一个事件处理程序附加到
#do
按钮上的click
事件。然后,当您实际单击该按钮时,将创建一条引用事件处理函数的
消息
,该消息将添加到消息队列
中。当事件循环
到达此消息时,它会在堆栈上创建一个框架
,并在jsfiddle中调用单击事件处理程序的函数。这就是有趣的地方。我们习惯于认为 Javascript 是异步的,以至于我们很容易忽视这个微小的事实:在执行下一帧之前,必须完整执行任何帧。没有并发,人们。
这意味着什么?这意味着每当从消息队列调用一个函数时,它都会阻塞队列,直到它生成的堆栈被清空为止。或者,更一般地说,它会阻塞,直到函数返回。它会阻止一切,包括 DOM 渲染操作、滚动等等。如果您想确认,只需尝试增加小提琴中长时间运行操作的持续时间(例如,再运行外循环 10 次),您会注意到,当它运行时,您无法滚动页面。如果它运行足够长的时间,您的浏览器会询问您是否要终止该进程,因为它会使页面无响应。该帧正在执行,事件循环和消息队列被卡住,直到它完成。
那么为什么文本没有更新的副作用呢?因为当您更改 DOM 中元素的值时,您可以在更改后立即
console.log()
其值,并看到它已< /strong> 已更改(这说明了为什么 DVK 的解释不正确)——浏览器正在等待堆栈耗尽(on
处理函数返回),从而等待消息完成,因此它最终可以执行运行时添加的消息,作为对我们的突变操作的反应,并在 UI 中反映该突变。这是因为我们实际上正在等待代码完成运行。我们还没有说过“有人获取这个,然后用结果调用这个函数,谢谢,现在我已经完成了,所以我要返回,现在就做任何事情”,就像我们通常对基于事件的异步 JavaScript 所做的那样。我们进入一个点击事件处理函数,我们更新一个DOM元素,我们调用另一个函数,另一个函数工作很长时间然后返回,然后我们更新同一个DOM元素,然后然后我们返回从初始函数开始,有效地清空堆栈。然后浏览器就可以获取队列中的下一条消息,这很可能是我们通过触发某些内部“on-DOM-mutation”类型事件生成的消息。
在当前执行的帧完成(函数返回)之前,浏览器 UI 无法(或选择不)更新 UI。就我个人而言,我认为这是设计使然,而不是限制。
那么为什么 setTimeout 会起作用呢?这样做是因为它有效地从自己的框架中删除了对长时间运行的函数的调用,安排它稍后在
window
上下文中执行,以便它本身可以立即返回< /strong> 并允许消息队列处理其他消息。我们的想法是,当更改 DOM 中的文本时,我们在 Javascript 中触发的 UI“更新”消息现在位于为长时间运行的函数排队的消息之前,以便 UI 更新在我们阻止之前发生许久。请注意,a) 长时间运行的函数在运行时仍然会阻塞所有内容,b) 您无法保证 UI 更新实际上在消息队列中领先于它。在我的 2018 年 6 月 Chrome 浏览器上,值
0
并不能“修复”小提琴演示的问题 - 10 可以。我实际上对此感到有点窒息,因为对我来说,UI 更新消息应该在它之前排队似乎是合乎逻辑的,因为它的触发器是在安排长时间运行的函数“稍后”运行之前执行的。但也许 V8 引擎中有一些优化可能会产生干扰,或者也许我的理解还不够。好的,那么使用
setTimeout
有什么问题,对于这种特殊情况有什么更好的解决方案呢?首先,在任何像这样的事件处理程序上使用 setTimeout 来尝试缓解另一个问题的问题很容易与其他代码混淆。这是我工作中的一个真实示例:
一位同事对事件循环有错误的理解,试图通过让一些模板渲染代码使用
setTimeout 0
进行渲染来“线程化”Javascript。他不再来这里询问,但我可以推测,也许他插入了计时器来测量渲染速度(这将是函数的返回即时性),并发现使用这种方法将使该函数的响应速度极快。第一个问题很明显;你不能线程化 javascript,所以当你添加混淆时你什么也得不到。其次,您现在已经有效地将模板的渲染与可能的事件侦听器堆栈分离,这些事件侦听器可能期望该模板已被渲染,但它很可能没有被渲染。该函数的实际行为现在是不确定的,就像任何运行它或依赖它的函数一样(在不知不觉中也是如此)。您可以做出有根据的猜测,但无法正确编码其行为。
编写依赖于其逻辑的新事件处理程序时的“修复”是还使用
setTimeout 0
。但是,这不是一个解决方案,它很难理解,而且调试由这样的代码引起的错误也没有什么乐趣。有时从来没有问题,有时它会持续失败,有时它会工作并偶尔中断,具体取决于平台的当前性能以及当时发生的其他情况。这就是为什么我个人建议不要使用这个 hack(它是一个 hack,我们都应该知道它是),除非你真的知道你在做什么以及后果是什么。但是我们可以做什么呢?好吧,正如引用的 MDN 文章所建议的那样,要么将工作拆分为多个消息(如果可以的话),以便排队的其他消息可以与您的工作交错并在其运行时执行,或者使用可以运行的 Web Worker与您的页面协同工作,并在完成计算后返回结果。
哦,如果您在想,“好吧,我不能在长时间运行的函数中添加一个回调以使其异步吗?”那么不行。回调不会使其异步,它仍然必须在显式调用回调之前运行长时间运行的代码。
Both of these two top-rated answers are wrong. Check out the MDN description on the concurrency model and the event loop, and it should become clear what's going on (that MDN resource is a real gem). And simply using
setTimeout
can be adding unexpected problems in your code in addition to "solving" this little problem.What's actually going on here is not that "the browser might not be quite ready yet because concurrency," or something based on "each line is an event that gets added to the back of the queue".
The jsfiddle provided by DVK indeed illustrates a problem, but his explanation for it isn't correct.
What's happening in his code is that he's first attaching an event handler to the
click
event on the#do
button.Then, when you actually click the button, a
message
is created referencing the event handler function, which gets added to themessage queue
. When theevent loop
reaches this message, it creates aframe
on the stack, with the function call to the click event handler in the jsfiddle.And this is where it gets interesting. We're so used to thinking of Javascript as being asynchronous that we're prone to overlook this tiny fact: Any frame has to be executed, in full, before the next frame can be executed. No concurrency, people.
What does this mean? It means that whenever a function is invoked from the message queue, it blocks the queue until the stack it generates has been emptied. Or, in more general terms, it blocks until the function has returned. And it blocks everything, including DOM rendering operations, scrolling, and whatnot. If you want confirmation, just try to increase the duration of the long running operation in the fiddle (e.g. run the outer loop 10 more times), and you'll notice that while it runs, you cannot scroll the page. If it runs long enough, your browser will ask you if you want to kill the process, because it's making the page unresponsive. The frame is being executed, and the event loop and message queue are stuck until it finishes.
So why this side-effect of the text not updating? Because while you have changed the value of the element in the DOM — you can
console.log()
its value immediately after changing it and see that it has been changed (which shows why DVK's explanation isn't correct) — the browser is waiting for the stack to deplete (theon
handler function to return) and thus the message to finish, so that it can eventually get around to executing the message that has been added by the runtime as a reaction to our mutation operation, and in order to reflect that mutation in the UI.This is because we are actually waiting for code to finish running. We haven't said "someone fetch this and then call this function with the results, thanks, and now I'm done so imma return, do whatever now," like we usually do with our event-based asynchronous Javascript. We enter a click event handler function, we update a DOM element, we call another function, the other function works for a long time and then returns, we then update the same DOM element, and then we return from the initial function, effectively emptying the stack. And then the browser can get to the next message in the queue, which might very well be a message generated by us by triggering some internal "on-DOM-mutation" type event.
The browser UI cannot (or chooses not to) update the UI until the currently executing frame has completed (the function has returned). Personally, I think this is rather by design than restriction.
Why does the
setTimeout
thing work then? It does so, because it effectively removes the call to the long-running function from its own frame, scheduling it to be executed later in thewindow
context, so that it itself can return immediately and allow the message queue to process other messages. And the idea is that the UI "on update" message that has been triggered by us in Javascript when changing the text in the DOM is now ahead of the message queued for the long-running function, so that the UI update happens before we block for a long time.Note that a) The long-running function still blocks everything when it runs, and b) you're not guaranteed that the UI update is actually ahead of it in the message queue. On my June 2018 Chrome browser, a value of
0
does not "fix" the problem the fiddle demonstrates — 10 does. I'm actually a bit stifled by this, because it seems logical to me that the UI update message should be queued up before it, since its trigger is executed before scheduling the long-running function to be run "later". But perhaps there're some optimisations in the V8 engine that may interfere, or maybe my understanding is just lacking.Okay, so what's the problem with using
setTimeout
, and what's a better solution for this particular case?First off, the problem with using
setTimeout
on any event handler like this, to try to alleviate another problem, is prone to mess with other code. Here's a real-life example from my work:A colleague, in a mis-informed understanding on the event loop, tried to "thread" Javascript by having some template rendering code use
setTimeout 0
for its rendering. He's no longer here to ask, but I can presume that perhaps he inserted timers to gauge the rendering speed (which would be the return immediacy of functions) and found that using this approach would make for blisteringly fast responses from that function.First problem is obvious; you cannot thread javascript, so you win nothing here while you add obfuscation. Secondly, you have now effectively detached the rendering of a template from the stack of possible event listeners that might expect that very template to have been rendered, while it may very well not have been. The actual behaviour of that function was now non-deterministic, as was — unknowingly so — any function that would run it, or depend on it. You can make educated guesses, but you cannot properly code for its behaviour.
The "fix" when writing a new event handler that depended on its logic was to also use
setTimeout 0
. But, that's not a fix, it is hard to understand, and it is no fun to debug errors that are caused by code like this. Sometimes there's no problem ever, other times it concistently fails, and then again, sometimes it works and breaks sporadically, depending on the current performance of the platform and whatever else happens to going on at the time. This is why I personally would advise against using this hack (it is a hack, and we should all know that it is), unless you really know what you're doing and what the consequences are.But what can we do instead? Well, as the referenced MDN article suggests, either split the work into multiple messages (if you can) so that other messages that are queued up may be interleaved with your work and executed while it runs, or use a web worker, which can run in tandem with your page and return results when done with its calculations.
Oh, and if you're thinking, "Well, couldn't I just put a callback in the long-running function to make it asynchronous?," then no. The callback doesn't make it asynchronous, it'll still have to run the long-running code before explicitly calling your callback.
如果您不想观看整个视频,这里有一个简单的解释为了能够理解这个问题的答案,我们需要理解:
从现在开始,我们正在讨论“浏览器中”的 JavaScript。像
setTimeout
这样的东西确实是浏览器的东西,而不是 JavaScript 本身的一部分。现在我们将另一个空间称为第二空间。
fn();
调用不等于setTimeout(fn, 0);
调用,如下所示下面进一步解释。我们首先假设另一个延迟,而不是
0
延迟,例如 5000 毫秒:setTimeout(fn, 5000);
。需要注意的是,这仍然是一个“函数调用”,所以它必须放在主空间上,并在完成后从主空间中删除,但是等等!,我们不喜欢整个漫长而无聊的 5 秒延迟。这会阻塞主空间,并且不允许 JavaScript 同时运行任何其他东西。值得庆幸的是,这并不是浏览器设计者设计它们的工作方式。 相反,这个调用(
setTimeout(fn, 5000);
)立即完成。这一点非常重要:即使有 5000 毫秒的延迟,这个函数调用也是瞬间完成的! 接下来会发生什么?它被从主空间中移除。它将被放置在哪里? (因为我们不想失去它)。您可能猜对了:浏览器听到这个调用并将其放在第二个空格上。浏览器会跟踪 5 秒的延迟,一旦超过,它就会查看主空间,然后“当它为空时”,将
fn();
回电。这就是setTimeout
的工作原理。所以,回到setTimeout(fn, 0),即使延迟为零,这仍然是对浏览器的调用,浏览器立即听到它并拾取它,并将其放在仅当主空间再次为空时,而不是真正的 0 毫秒后,才将第二个空间放回主空间。
我真的建议您也观看该视频,因为他解释得非常好,并且更开放了技术问题。
If you don't want to watch a whole video, here's a simple explanation of the things one needs to understand, in order to be able to understand the answer to this question:
From this point on, we're talking about JavaScript "in browsers". Things like
setTimeout
are indeed browser things, and are not part of the JavaScript itself.Now let's call that other space the second space.
fn
is a function. The important thing to understand here is thatfn();
call is not equal to thesetTimeout(fn, 0);
call as will be explained further below.Instead of a
0
delay, let's assume another delay first, e.g., 5000 milliseconds:setTimeout(fn, 5000);
. It's important to note that this is still a "function call", so it has to be put on the main space, and removed from it when it's done, but wait!, we don't like a whole lengthy and boring 5 seconds delay. That would block the main space and will not allow JavaScript to run ANYTHING else in the meantime.Thankfully this is not how the browser designers designed them to work. Instead, this call(
setTimeout(fn, 5000);
) is done instantly. This is very important: Even with the 5000 milliseconds delay, this function call is complete in an instant! What will happen next? It gets removed from the main space. Where will it be put on? (because we don't want to lose it). You might have guessed right: The browser hears this call and puts it on the second space.The browser keeps track of the 5 seconds delay and once it's passed, it looks at the main space, and "WHEN IT'S EMPTY", puts the
fn();
call back on it. That is how thesetTimeout
works.So, back to the
setTimeout(fn, 0)
, even though the delay is zero, this is still a call to the browser, and the browser hears it instantly and picks it up, and puts it on the second space and puts it back on the main space only when the main space is empty again, and not really 0 milliseconds later.I really recommend watching that video as well since he's explained it really well, and opens technical things up more.
这样做的原因之一是将代码的执行推迟到单独的后续事件循环。当响应某种浏览器事件(例如鼠标单击)时,有时需要仅在处理当前事件之后才执行操作。
setTimeout()
工具是最简单的方法。编辑现在已经是 2015 年了,我应该注意到还有
requestAnimationFrame()
,它并不完全相同,但它与setTimeout(fn, 0) 足够接近
值得一提的是。再次编辑现在已经是 2024 年了,我应该注意到,您可以使用 Promise API 做一些事情,例如:
或者,更有趣的是:
这并不总是合适的,但有时是合适的。就我个人而言,我认为围绕计时器交互方式的细微差异进行设计是一件非常脆弱的事情。在某些奇怪的情况下你可能不得不这么做,但我建议选择你喜欢的并坚持下去。
One reason to do that is to defer the execution of code to a separate, subsequent event loop. When responding to a browser event of some kind (mouse click, for example), sometimes it's necessary to perform operations only after the current event is processed. The
setTimeout()
facility is the simplest way to do it.edit now that it's 2015 I should note that there's also
requestAnimationFrame()
, which isn't exactly the same but it's sufficiently close tosetTimeout(fn, 0)
that it's worth mentioning.edit again now that it's 2024 I should note that there are things you can do with the Promise API, such as:
or, fancier:
That's not always appropriate, but it is sometimes. Personally I think that designing around subtle differences in the way timers interact is a pretty fragile thing to do. You might have to in some weird situations, but I'd say pick one you like and stick to it.
这是一个老问题,也有老答案。我想对这个问题添加一个新的视角,并回答为什么会发生这种情况,而不是为什么它有用。
所以你有两个函数:
然后按以下顺序调用它们
f1(); f2();
只是为了看到第二个首先执行。原因如下:
setTimeout
不可能有 0 毫秒的时间延迟。 最小值由浏览器确定,并且不是 0 毫秒。 历史上< /a> 浏览器将此最小值设置为 10 毫秒,但是 HTML5 规范< /a> 和现代浏览器将其设置为 4 毫秒。同样来自 mozilla:
PS信息是在阅读以下文章后获取的。
This is an old questions with old answers. I wanted to add a new look at this problem and to answer why is this happens and not why is this useful.
So you have two functions:
and then call them in the following order
f1(); f2();
just to see that the second one executed first.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.Also from mozilla:
P.S. information is taken after reading the following article.
由于它传递的持续时间为
0
,我认为这是为了从执行流程中删除传递给setTimeout
的代码。因此,如果它是一个可能需要一段时间的函数,它不会阻止后续代码的执行。Since it is being passed a duration of
0
, I suppose it is in order to remove the code passed to thesetTimeout
from the flow of execution. So if it's a function that could take a while, it won't prevent the subsequent code from executing.这样做的另一件事是将函数调用推送到堆栈底部,以防止递归调用函数时堆栈溢出。这具有 while 循环的效果,但允许 JavaScript 引擎触发其他异步计时器。
The other thing this does is push the function invocation to the bottom of the stack, preventing a stack overflow if you are recursively calling a function. This has the effect of a
while
loop but lets the JavaScript engine fire other asynchronous timers.通过调用 setTimeout,您可以给页面时间来对用户正在执行的操作做出反应。这对于页面加载期间运行的函数特别有用。
By calling setTimeout you give the page time to react to the whatever the user is doing. This is particularly helpful for functions run during page load.
setTimeout 有用的其他一些情况:
您想要将长时间运行的循环或计算分解为较小的组件,以便浏览器不会出现“冻结”或说“页面上的脚本正忙”。
您希望在单击时禁用表单提交按钮,但如果您在 onClick 处理程序中禁用该按钮,则将不会提交表单。时间为零的 setTimeout 可以解决问题,允许事件结束,表单开始提交,然后可以禁用您的按钮。
Some other cases where setTimeout is useful:
You want to break a long-running loop or calculation into smaller components so that the browser doesn't appear to 'freeze' or say "Script on page is busy".
You want to disable a form submit button when clicked, but if you disable the button in the onClick handler the form will not be submitted. setTimeout with a time of zero does the trick, allowing the event to end, the form to begin submitting, then your button can be disabled.
问题是您试图对不存在的元素执行 Javascript 操作。该元素尚未加载,
setTimeout()
通过以下方式为元素加载提供更多时间:setTimeout()
导致事件异步 因此在所有同步代码之后执行,为您的元素提供更多时间加载。异步回调如setTimeout()
中的回调被放入事件队列中,并在同步堆栈之后由事件循环入栈。代码为空。setTimeout()
中,作为第二个参数的 ms 值 0 通常会稍高一些(4-10ms,具体取决于浏览器)。执行setTimeout()
回调所需的时间稍长是由事件循环的“滴答”数量(如果堆栈为空,则滴答将回调推入堆栈)造成的。由于性能和电池寿命的原因,事件循环中的滴答次数被限制在每秒小于 1000 次。The problem was you were trying to perform a Javascript operation on a non existing element. The element was yet to be loaded and
setTimeout()
gives more time for an element to load in the following ways:setTimeout()
causes the event to be ansynchronous therefore being executed after all the synchronous code, giving your element more time to load. Asynchronous callbacks like the callback insetTimeout()
are placed in the event queue and put on the stack by the event loop after the stack of synchronous code is empty.setTimeout()
is often slightly higher (4-10ms depending on browser). This slightly higher time needed for executing thesetTimeout()
callbacks is caused by the amount of 'ticks' (where a tick is pushing a callback on the stack if stack is empty) of the event loop. Because of performance and battery life reasons the amount of ticks in the event loop are restricted to a certain amount less than 1000 times per second.关于执行循环和在其他代码完成之前渲染 DOM 的答案是正确的。 JavaScript 中的零秒超时有助于使代码成为伪多线程,尽管事实并非如此。
我想补充一点,JavaScript 中跨浏览器/跨平台零秒超时的最佳值实际上约为 20 毫秒,而不是 0(零),因为许多移动浏览器由于时钟限制而无法注册小于 20 毫秒的超时在 AMD 芯片上。
此外,不涉及 DOM 操作的长时间运行的进程现在应该发送给 Web Workers,因为它们提供了真正的 JavaScript 多线程执行。
The answers about execution loops and rendering the DOM before some other code completes are correct. Zero second timeouts in JavaScript help make the code pseudo-multithreaded, even though it is not.
I want to add that the BEST value for a cross browser / cross platform zero-second timeout in JavaScript is actually about 20 milliseconds instead of 0 (zero), because many mobile browsers can't register timeouts smaller than 20 milliseconds due to clock limitations on AMD chips.
Also, long-running processes that do not involve DOM manipulation should be sent to Web Workers now, as they provide true multithreaded execution of JavaScript.
setTimout on 0 在设置延迟承诺(您希望立即返回)的模式中也非常有用:
setTimout on 0 is also very useful in the pattern of setting up a deferred promise, which you want to return right away:
Javascript 是单线程应用程序,因此不允许同时运行函数,因此要使用事件循环来实现此目的。那么 setTimeout(fn, 0) 到底做了什么,它被放入任务任务中,该任务任务在调用堆栈为空时执行。我知道这个解释很无聊,所以我建议您观看此视频,这将帮助您了解浏览器中的工作原理。
观看此视频:- https://www.youtube.com/watch? time_continue=392&v=8aGhZQkoFbQ
Javascript is single threaded application so that don't allow to run function concurrently so to achieve this event loops are use. So exactly what setTimeout(fn, 0) do that its pussed into task quest which is executed when your call stack is empty. I know this explanation is pretty boring, so i recommend you to go through this video this will help you how things work under the hood in browser.
Check out this video:- https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ
在问题中,存在竞争条件:
您的代码始终赢得这场比赛,并尝试在浏览器准备就绪之前设置下拉选择,这意味着会出现错误。
这场竞赛的存在是因为 JavaScript 有一个单执行线程,即与页面渲染共享。实际上,运行 JavaScript 会阻止 DOM 的更新。
您的解决方法是:
使用回调调用
setTimeout
,并将零作为第二个参数,将安排回调在最短的可能延迟(大约 10 毫秒)之后异步运行当选项卡具有焦点并且 JavaScript 执行线程不忙时。因此,OP 的解决方案是将所选索引的设置延迟大约 10 毫秒。这给了浏览器一个初始化 DOM 的机会,修复了 bug。
每个版本的 Internet Explorer 都表现出奇怪的行为,有时这种解决方法是必要的。或者,它可能是 OP 代码库中的一个真正的错误。
请参阅 Philip Roberts 的演讲“事件循环到底是什么?”,了解更全面的解释。
In the question, there existed a race condition between:
Your code was consistently winning this race and attempting to set drop-down selection before the browser was ready, meaning that the bug would appear.
This race existed because JavaScript has a single thread of execution that is shared with page rendering. In effect, running JavaScript blocks the updating of the DOM.
Your workaround was:
Invoking
setTimeout
with a callback, and zero as the second argument will schedule the callback to be run asynchronously, after the shortest possible delay - which will be around 10ms when the tab has focus and the JavaScript thread of execution is not busy.The OP's solution, therefore was to delay by about 10ms, the setting of the selected index. This gave the browser an opportunity to initialize the DOM, fixing the bug.
Every version of Internet Explorer exhibited quirky behaviors and this kind of workaround was necessary at times. Alternatively it might have been a genuine bug in the OP's codebase.
See Philip Roberts talk "What the heck is the event loop?" for more thorough explanation.
前言:
其他一些答案是正确的,但实际上并没有说明要解决的问题是什么,因此我创建了这个答案来展示详细说明。
因此,我将发布详细介绍浏览器的功能以及使用
setTimeout()
如何提供帮助。它看起来有点长,但实际上非常简单明了——我只是把它做得非常详细。更新:我制作了一个 JSFiddle 来实时演示以下解释:http:// jsfiddle.net/C2YBE/31/ 。非常感谢@ThangChung 帮助启动它。
更新2:以防万一 JSFiddle 网站崩溃或删除代码,我在最后将代码添加到了这个答案中。
细节:
想象一个带有“做某事”按钮和结果 div 的网络应用程序。
“做某事”按钮的
onClick
处理程序调用函数“LongCalc()”,该函数执行 2 件事:进行非常长的计算(假设需要 3 分钟)
将计算结果打印到结果 div 中。
现在,你的用户开始测试这个,点击“做某事”按钮,页面在那里似乎没有做任何事情 3 分钟,他们变得焦躁不安,再次点击按钮,等待 1 分钟,没有任何反应,再次点击按钮
......问题很明显 - 你需要一个“状态”DIV,它显示正在发生的事情。让我们看看它是如何工作的。
因此,您添加一个“状态”DIV(最初为空),并修改
onclick
处理程序(函数LongCalc()
)来执行 4 件事:填充状态“正在计算...可能需要约 3 分钟”状态 DIV
进行很长的计算(例如需要 3 分钟)
p>
将计算结果打印到结果 div 中。
将状态“计算完成”填充到状态 DIV
并且,您很乐意将应用程序交给用户重新测试。
他们回来找你时看起来很生气。并解释说,当他们单击按钮时,状态 DIV 从未更新为“正在计算...”状态!!!
您摸不着头脑,在 StackOverflow 上四处询问(或阅读文档或谷歌),并认识到问题:
浏览器将事件产生的所有“TODO”任务(UI 任务和 JavaScript 命令)放入单个队列中。不幸的是,使用新的“计算...”值重新绘制“状态”DIV 是一个单独的 TODO,它会到达队列的末尾!
以下是用户测试期间事件的详细信息以及每个事件后队列的内容:
[Empty]
[执行 OnClick 处理程序(第 1-4 行)]
[执行 OnClick 处理程序(第 2-4 行),使用新的“计算”值重新绘制 Status DIV]
。 请注意,虽然 DOM 更改是瞬时发生的,但要重新绘制相应的 DOM 元素,您需要一个由 DOM 更改触发的新事件,该事件位于队列末尾。[执行 OnClick 处理程序(第 3-4 行),使用“计算”值重新绘制 Status DIV]
。[执行 OnClick 处理程序(第 4 行),使用“计算”值重新绘制 Status DIV,使用结果重新绘制结果 DIV]
。[执行OnClick处理程序,使用“计算”值重新绘制状态DIV,使用结果重新绘制结果DIV;重新绘制带有“DONE”值的 Status DIV]
。onclick
处理程序子执行隐含的return
。我们将“执行 OnClick 处理程序”从队列中取出,并开始执行队列中的下一个项目。眼尖的观众甚至可能会注意到“状态 DIV 与“计算”值闪烁了不到一微秒 - 计算完成后
因此,根本问题是“状态”DIV 的重新绘制事件在需要 3 分钟的“执行第 2 行”事件之后被放置在队列的末尾,因此直到计算完成之后才会发生实际的重新绘制
。 它有什么帮助?因为通过
setTimeout
调用长时间执行的代码,您实际上创建了 2 个事件:setTimeout
执行本身,以及(由于 0 超时) ),为正在执行的代码提供单独的队列条目,因此,为了解决您的问题,您可以将
onClick
处理程序修改为两个语句(在新函数中或仅在onClick
中的一个块中)。 code>):将状态“正在计算...可能需要约 3 分钟”填充到状态 DIV
以 0 超时执行
setTimeout()
并调用LongCalc()
函数。LongCalc()
函数与上次几乎相同,但显然没有将“正在计算...”状态 DIV 更新作为第一步;而是立即开始计算。那么,事件序列和队列现在是什么样子的呢?
[Empty]
[执行 OnClick 处理程序(状态更新,setTimeout() 调用)]
[执行 OnClick 处理程序(这是一个 setTimeout 调用),使用新的“计算”值重新绘制 Status DIV]
。[使用“正在计算”值重新绘制 Status DIV]
。队列中再有 0 秒没有任何新内容。[使用“计算”值重新绘制 Status DIV,执行 LongCalc(第 1-3 行)]
。[执行 LongCalc(第 1-3 行)]
。请注意,此重新绘制事件实际上可能在警报响起之前发生,这也同样有效。万岁!在计算开始之前,状态 DIV 刚刚更新为“正在计算...”!
下面是来自 JSFiddle 的示例代码,说明了这些示例: http://jsfiddle.net/C2YBE/31/ :
HTML 代码:
JavaScript 代码:(在
onDomReady
上执行,可能需要 jQuery 1.9)Preface:
Some of the other answers are correct but don't actually illustrate what the problem being solved is, so I created this answer to present that detailed illustration.
As such, I am posting a detailed walk-through of what the browser does and how using
setTimeout()
helps. It looks longish but is actually very simple and straightforward - I just made it very detailed.UPDATE: I have made a JSFiddle to live-demonstrate the explanation below: http://jsfiddle.net/C2YBE/31/ . Many thanks to @ThangChung for helping to kickstart it.
UPDATE2: Just in case JSFiddle web site dies, or deletes the code, I added the code to this answer at the very end.
DETAILS:
Imagine a web app with a "do something" button and a result div.
The
onClick
handler for "do something" button calls a function "LongCalc()", which does 2 things:Makes a very long calculation (say takes 3 min)
Prints the results of calculation into the result div.
Now, your users start testing this, click "do something" button, and the page sits there doing seemingly nothing for 3 minutes, they get restless, click the button again, wait 1 min, nothing happens, click button again...
The problem is obvious - you want a "Status" DIV, which shows what's going on. Let's see how that works.
So you add a "Status" DIV (initially empty), and modify the
onclick
handler (functionLongCalc()
) to do 4 things:Populate the status "Calculating... may take ~3 minutes" into status DIV
Makes a very long calculation (say takes 3 min)
Prints the results of calculation into the result div.
Populate the status "Calculation done" into status DIV
And, you happily give the app to users to re-test.
They come back to you looking very angry. And explain that when they clicked the button, the Status DIV never got updated with "Calculating..." status!!!
You scratch your head, ask around on StackOverflow (or read docs or google), and realize the problem:
The browser places all its "TODO" tasks (both UI tasks and JavaScript commands) resulting from events into a single queue. And unfortunately, re-drawing the "Status" DIV with the new "Calculating..." value is a separate TODO which goes to the end of the queue!
Here's a breakdown of the events during your user's test, contents of the queue after each event:
[Empty]
[Execute OnClick handler(lines 1-4)]
[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]
. Please note that while the DOM changes happen instantaneously, to re-draw the corresponding DOM element you need a new event, triggered by the DOM change, that went at the end of the queue.[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
.[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
.[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
.return
fromonclick
handler sub. We take the "Execute OnClick handler" off the queue and start executing next item on the queue.Sharp-eyed viewers might even notice "Status DIV with "Calculating" value flashing for fraction of a microsecond - AFTER THE CALCULATION FINISHED
So, the underlying problem is that the re-draw event for "Status" DIV is placed on the queue at the end, AFTER the "execute line 2" event which takes 3 minutes, so the actual re-draw doesn't happen until AFTER the calculation is done.
To the rescue comes the
setTimeout()
. How does it help? Because by calling long-executing code viasetTimeout
, you actually create 2 events:setTimeout
execution itself, and (due to 0 timeout), separate queue entry for the code being executed.So, to fix your problem, you modify your
onClick
handler to be TWO statements (in a new function or just a block withinonClick
):Populate the status "Calculating... may take ~3 minutes" into status DIV
Execute
setTimeout()
with 0 timeout and a call toLongCalc()
function.LongCalc()
function is almost the same as last time but obviously doesn't have "Calculating..." status DIV update as first step; and instead starts the calculation right away.So, what does the event sequence and the queue look like now?
[Empty]
[Execute OnClick handler(status update, setTimeout() call)]
[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
.[re-draw Status DIV with "Calculating" value]
. The queue has nothing new in it for 0 more seconds.[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
.[execute LongCalc (lines 1-3)]
. Please note that this re-draw event might actually happen BEFORE the alarm goes off, which works just as well.Hooray! The Status DIV just got updated to "Calculating..." before the calculation started!!!
Below is the sample code from the JSFiddle illustrating these examples: http://jsfiddle.net/C2YBE/31/ :
HTML code:
JavaScript code: (Executed on
onDomReady
and may require jQuery 1.9)请参阅 John Resig 关于 JavaScript 计时器工作原理 的文章。当您设置超时时,它实际上会将异步代码排队,直到引擎执行当前的调用堆栈。
Take a look at John Resig's article about How JavaScript Timers Work. When you set a timeout, it actually queues the asynchronous code until the engine executes the current call stack.
setTimeout()
会为您争取一些时间,直到加载 DOM 元素,即使设置为 0。请查看:setTimeout
setTimeout()
buys you some time until the DOM elements are loaded, even if is set to 0.Check this out: setTimeout