这两个排名靠前的答案都是错的。查看关于并发模型和事件循环的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,它可以与你的页面一起运行,并在计算完成时返回结果。
哦,如果你在想,“我能不能在长时间运行的函数中加入一个回调,使它异步?,然后没有。回调并没有使它异步化,在显式调用回调之前,它仍然必须运行长时间运行的代码。