JS事件队列中事件的顺序

use*_*527 5 javascript asynchronous javascript-events settimeout

(我相信)了解的事情

尽管现代浏览器是多线程的,但JavaScript是单线程执行的,并且可以保证同步执行。这意味着,如果输入了调用堆栈,则JS会不间断运行,直到调用堆栈再次变空。特别是,可以确保没有两个JS脚本同时运行。

但是,JavaScript是事件驱动的。根据MDN上并发模型描述,浏览器维护事件队列。如果空闲,浏览器将从队列中获取第一个事件并执行相关联的JS(如果有)。脚本终止后,浏览器再次获得控制权并继续进行下一个事件。执行JS脚本时,浏览器不会执行其他任何操作,尤其是它会阻止UI。后者是臭名昭著的“长时间运行脚本警告”的原因。

setTimeout运行系统(又名浏览器)提供了一些真正的异步功能。它们可以用于将新事件放在事件队列中,并且如果事件已被触发并且调用栈为空,则关联的回调将被放在调用栈中。如果不使用本机异步功能之一,开发人员就无法创建自己的“自己的”异步功能。

话虽如此,setTimeout( someFunction, 0 )它提供了一个选项,该选项如何将长时间运行的代码片段分割成小段,否则这些代码会触发“长时间运行的脚本警告”。如果调用堆栈为空,则浏览器可以在执行下一段代码之前赶上其他事件(例如UI)。

一个例子

为了检查我是否正确,我创建了这段代码

"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
  );
}
Run Code Online (Sandbox Code Playgroud)

预期成绩

我希望警报框以正确的顺序出现: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),并且仅混合了回调,那么我将接受事件队列不能保证事件顺序。但是,似乎甚至同步循环也被异步事件中断和交错。我认为这不会发生。这里发生了什么?

Ale*_*ara 3

这是因为在某些浏览器中(在您的例子中是 Firefox),传统的阻塞alertconfirmprompt函数不会停止事件队列。它们确实会停止执行当前的代码,直到用户与其交互,但循环会继续运行,并且一些JavaScript 事件会继续触发(但不是所有事件)。

这是 Firefox 的错误吗?

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

在规范中,暂停是可选的:

(可选)在等待用户确认消息时暂停。

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

另一个例子:

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

(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);
})();
Run Code Online (Sandbox Code Playgroud)