【问题标题】:Order of events in JS event queue [duplicate]JS事件队列中的事件顺序[重复]
【发布时间】: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)中,传统上的阻塞函数如 alertconfirm 是半异步的(UI 不块,并且某些事件会触发)。也许这就是原因?
  • 看看这个更新的小提琴:jsfiddle.net/5kzv973x/4 - 浏览器趁机重绘屏幕,同时等待您关闭confirm()
  • @Alexander O'Mara:是的,我使用的是 Firefox。也许问题真的与 alert() 是“半异步的”有关。 nnnnnn 更新的小提琴在 Firefox 中按预期工作。那么,这是 Firefox 的错误吗?

标签: javascript asynchronous dom-events settimeout


【解决方案1】:

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

这是 Firefox 的错误吗?

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

In the spec,暂停是可选的:

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

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);
})();

【讨论】:

  • 但这也意味着“不能同时执行两个 JS 脚本”这一简单规则并不适用于所有情况。在上面的示例中,如果外部循环仍在运行但在同步 alerts() 之一处被阻塞,然后与异步回调交错,则可能存在其他情况,这些回调会修改休眠代码的变量,因此可以导致所有众所周知的并发问题。我说的对吗?
  • @user2690527 不完全是。是的,可以在alert 打开时修改变量,规范不允许这样做(我在答案中添加了一个示例)。然而,这并不意味着从技术上讲有 2 段代码同时执行,暂停的一段代码的执行将暂停,直到 alert-type 对话框关闭,并重新添加到队列中。没有两段代码会同时执行,这主要是保证不会发生。还有其他方法可以意外触发其他代码,例如调用.click() 会触发onclick 处理程序。
猜你喜欢
  • 1970-01-01
  • 2021-08-24
  • 1970-01-01
  • 2012-07-10
  • 1970-01-01
  • 2020-02-29
  • 1970-01-01
  • 2021-11-10
  • 2018-09-18
相关资源
最近更新 更多