【发布时间】:2016-12-05 07:52:25
【问题描述】:
事情(我相信)理解
尽管现代浏览器是多线程的,但 JavaScript 是单线程执行的,并且保证同步执行。这意味着如果进入调用堆栈,JS 会不间断地运行,直到调用堆栈再次变空。特别是保证不会有两个 JS 脚本同时运行。
然而,JavaScript 是事件驱动的。根据description of the concurrency model at MDN,浏览器维护一个事件队列。如果空闲,浏览器从队列中获取第一个事件并执行相关联的 JS(如果有)。脚本终止后,浏览器再次获得控制权并继续执行下一个事件。执行 JS 脚本时,浏览器不会做任何其他事情,尤其是它会阻塞 UI。后者负责臭名昭著的“长时间运行脚本警告”。
运行时系统(又名浏览器)提供了一些真正的异步函数,例如setTimeout。它们可用于将新事件放入事件队列,如果事件已被触发且调用堆栈为空,则关联的回调将被放入调用堆栈。如果不使用本机异步函数之一,开发人员无法创建“自己的”异步函数。
话虽如此,setTimeout( someFunction, 0 ) 提供了一个选项,如何将长时间运行的代码分割成更小的部分,否则会触发“长时间运行的脚本警告”。如果调用堆栈变空,浏览器可以在执行下一段代码之前赶上其他事件(如 UI)。
一个例子
为了检查我是否正确,我创建了这个piece of code
"use strict";
for( var i = 0; i != 5; i++ ) {
confirm( 'This is #: ' + i );
setTimeout(
( function(j) {
return function() { confirm( 'This is #: ' + j ); };
} )( i+10 ),
0
);
}
预期结果
我希望警报框以正确的顺序出现:0、1、2、3、4、10、11、12、13 和 14。我的想法是在每次迭代中都会发生以下两个步骤:(a ) 同步显示第一个确认框。 (b) 一个新事件被推到事件队列的末尾。但是,回调还没有被调用,而是被推迟了,因为调用堆栈不是空的,因为循环仍在运行。
因此,我希望按照该顺序首先看到警报 0、1、2、3、4,因为这是同步循环的一部分。然后循环终止,调用堆栈变得清晰。下一个事件取自恰好是第一个异步超时的事件队列。所以我预计接下来会看到 10 个。确认对话框关闭后,调用堆栈再次变得清晰。所以我预计接下来会看到 11 个。以此类推。
实际结果
警报框以混合顺序显示,例如0、1、2、10、11、3、12、4、13、14。
问题
为什么警报框的顺序是混合的?如果至少同步部分 (0, 1, 2, 3, 4) 被整体显示并且只有回调被混淆,我会接受事件队列不保证事件的顺序。但是,似乎即使是同步循环也会被中断并与异步事件交错。我认为这保证不会发生。这是怎么回事?
【问题讨论】:
-
您在哪个浏览器中测试?当我在 Chrome 中运行你的小提琴时,它似乎始终如一(我尝试了大约十次)首先显示 0、1、2、3、4,但随后 >=10 提示的顺序发生了变化。 (请注意,如果您使用
console.log()而不是confirm(),您应该得到您期望的顺序。) -
我目前无法对此进行测试,但在某些浏览器(无论如何是 Firefox)中,传统上的阻塞函数如
alert和confirm是半异步的(UI 不块,并且某些事件会触发)。也许这就是原因? -
看看这个更新的小提琴:jsfiddle.net/5kzv973x/4 - 浏览器趁机重绘屏幕,同时等待您关闭
confirm()。 -
@Alexander O'Mara:是的,我使用的是 Firefox。也许问题真的与 alert() 是“半异步的”有关。 nnnnnn 更新的小提琴在 Firefox 中按预期工作。那么,这是 Firefox 的错误吗?
标签: javascript asynchronous dom-events settimeout