JS事件队列中的事件顺序

Order of events in JS event queue

事情(相信)明白

虽然现代浏览器都是多线程的,但是JavaScript是单线程执行的,保证同步执行。这意味着如果调用堆栈进入 JS 运行s 不间断,直到调用堆栈再次变空。特别是,保证没有两个JS脚本运行并发

但是,JavaScript 是事件驱动的。根据 description of the concurrency model at MDN 浏览器维护一个事件队列。如果空闲,浏览器从队列中取出第一个事件并执行相关的 JS 片段(如果有的话)。脚本终止后,浏览器再次获得控制权并继续处理下一个事件。执行 JS 脚本时,浏览器不会执行任何其他操作,尤其是它会阻止 UI。后者是臭名昭著的 "long-running script warning".

的罪魁祸首

运行 时间系统(又名浏览器)提供了一些真正的异步功能,例如 setTimeout。它们可用于将新事件放入事件队列,如果事件已被触发且调用堆栈为空,则关联的回调将放入调用堆栈。开发人员无法在不使用本机异步函数之一的情况下创建他的 "own" 异步函数。

话虽这么说,setTimeout( someFunction, 0 ) 提供了一个选项,如何将长 运行ning 的代码片段拆分成更小的片段,否则会触发 "long-running script warning"。如果调用堆栈变空,浏览器可以在执行下一段代码之前赶上其他事件(如 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) 一个新事件被推到事件队列的末尾。然而,回调还没有被调用,而是被推迟了,因为调用堆栈不为空,因为循环仍在 运行ning.

因此,我希望按顺序首先看到警报 0、1、2、3、4,因为这是同步循环的一部分。然后循环终止,调用堆栈变得清晰。下一个事件从事件队列中取出,恰好是第一个异步超时。所以我预计接下来会看到 10 个。确认对话框关闭后,调用堆栈再次变得清晰。所以我预计接下来会看到 11。等等。

实际结果

警告框以混合顺序显示,例如0、1、2、10、11、3、12、4、13、14。

问题

为什么警告框的顺序混乱?如果至少同步部分(0、1、2、3、4)被完整显示并且只有回调被混淆,我会接受事件队列不能保证事件的顺序。但是,似乎即使是同步循环也会被中断并与异步事件交织在一起。我以为这保证不会发生。这是怎么回事?

这是因为在某些浏览器(在您的情况下是 Firefox)中,传统上阻塞 alertconfirmprompt 函数不会停止事件队列。它们会停止执行当前代码段,直到用户与之交互,但循环会继续 运行 和 some JavaScript 事件继续触发(不是尽管所有事件)。

Is this a Firefox bug?

不,您的代码只是有未定义的行为。

In the spec,暂停是可选的:

Optionally, pause while waiting for the user to acknowledge the message.

Firefox 选择不这样做是为了改善用户体验。

另一个例子:

这是一个简单的示例,显示调整大小事件仍然如何触发。这表明您无法确定在 alert 类型的对话框打开时不会修改事件处理程序可访问的变量。

(function(){
    'use strict';
    var resized = false;
    window.addEventListener('resize', function() {
        resized = true;
        console.log('resizing');
    });
    alert('resize the window before continuing');
    console.log('Resize events fired while alert open?: ' + resized);
})();