我最近遇到了一个相当严重的错误,其中代码通过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()?


当前回答

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

其他回答

setTimeout有用的其他一些情况:

你想要把一个长时间运行的循环或计算分解成更小的组件,这样浏览器就不会出现“冻结”或说“页面上的脚本正在忙”。

您希望在单击时禁用表单提交按钮,但如果禁用onClick处理程序中的按钮,则表单将无法提交。setTimeout的时间为0,允许事件结束,表单开始提交,然后可以禁用按钮。

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

这里实际发生的事情并不是“由于并发性,浏览器可能还没有完全准备好”,或者基于“每一行都是一个被添加到队列后面的事件”。

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

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

然后,当您实际单击按钮时,将创建引用事件处理程序函数的消息,该消息被添加到消息队列中。当事件循环到达这条消息时,它在堆栈上创建一个框架,并调用jsfiddle中的click事件处理程序。

这就是有趣的地方。我们习惯于认为Javascript是异步的,以至于我们很容易忽略这个小事实:在执行下一帧之前,任何一帧都必须完整地执行。不要并发,伙计们。

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 (the on 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.

浏览器UI不能(或选择不)更新UI,直到当前执行的帧已经完成(函数已经返回)。我个人认为,这与其说是限制,不如说是有意为之。

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 the window 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.

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

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

一个同事,在对事件循环的错误理解中,试图通过让一些模板呈现代码使用setTimeout 0进行呈现来“线程化”Javascript。他不再问这个问题了,但我可以推测,也许他插入了计时器来测量呈现速度(即函数返回的即时性),并发现使用这种方法将使该函数的响应速度非常快。

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.

但是我们能做什么呢?好吧,正如引用的MDN文章所建议的那样,要么将工作拆分为多个消息(如果可以的话),这样其他排队的消息就可以与你的工作交织在一起,并在它运行时执行,要么使用web worker,它可以与你的页面一起运行,并在计算完成时返回结果。

哦,如果你在想,“我能不能在长时间运行的函数中加入一个回调,使它异步?,然后没有。回调并没有使它异步化,在显式调用回调之前,它仍然必须运行长时间运行的代码。

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

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

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