【问题标题】:What was the motivation for introducing a separate microtask queue which the event loop prioritises over the task queue?引入事件循环优先于任务队列的单独微任务队列的动机是什么?
【发布时间】:2021-05-17 07:25:20
【问题描述】:

我对 JS 中异步任务如何调度的理解

如果我有任何错误,请纠正我:

JS 运行时引擎代理由事件循环驱动,该循环收集任何用户和其他事件,将任务排入队列以处理每个回调。

事件循环不断运行,有如下思考过程:

  • execution context stack(通常称为调用堆栈)是否为空?
  • 如果是,则将微任务队列(或作业队列)中的任何微任务插入调用堆栈。继续这样做,直到微任务队列为空。
  • 如果微任务队列为空,则将任务队列(或回调队列)中最旧的任务插入调用堆栈

因此,任务和微任务的处理方式有两个关键区别:

  • 微任务(例如 promises 使用微任务队列来运行其回调)优先于任务(例如来自其他 Web API 的回调,例如 setTimeout)
  • 此外,所有微任务都在任何其他事件处理或呈现或任何其他任务发生之前完成。因此,微任务之间的应用环境基本相同。

Promise 在ES6 2015 中引入。我假设 ES6 中也引入了微任务队列。

我的问题

引入微任务队列的动机是什么?为什么不继续使用任务队列来处理 Promise?

更新 #1 - 我正在寻找对规范进行此更改的明确历史原因 - 即它旨在解决的问题是什么,而不是关于微任务队列的好处的固执己见的答案。

参考资料:

【问题讨论】:

  • 我猜“微任务之间的应用程序环境基本相同”就可以了。通常,它允许链接同步事物 (Promise.resolve(1).then(x => x+1).then(console.log)) 的承诺代码立即运行,而不会被处理事件等更大的任务中断。它也可以通过一个循环服务多个队列和明确的优先级规则来完成。
  • 引入它的历史原因是使其成为 ECMAScript 规范的一部分,而事件循环是由嵌入器定义的功能(在 HTML 的情况下,由 WHATWG 指定)跨度>
  • @Bergi - 我建议你写一个答案。

标签: javascript asynchronous ecmascript-6 promise event-handling


【解决方案1】:

Promise 是在 ES6 2015 中引入的。我假设微任务队列也在 ES6 中引入。

实际上,ECMAScript 标准根本没有引入 microtask 任务队列:ES6 标准规定将已解决的 Promise 的 Promise 处理作业放入 TriggerPromiseReactions 下名为“PromiseJobs”的队列中,使用抽象进程 EnqueueJob 进入由宿主环境实现的作业队列中的作业,没有规定应该如何处理宿主队列。

在被 ECMAScript 采用之前

Promise 库是在用户领域开发的。执行 Promise 处理程序、监视它们是否抛出或返回值并有权访问 Promise 链中下一个 Promise 的 resolve 和 reject 函数的代码称为“trampoline”。虽然是 Promise 库的一部分,但蹦床不被视为用户代码的一部分,并且使用干净堆栈调用承诺处理程序的声明排除了蹦床占用的堆栈空间。

使用一系列处理程序来调用已解决状态(已完成或已拒绝)的 promise 的结算需要启动蹦床以运行 promise 作业(如果它尚未运行)。

使用空堆栈启动蹦床执行的方法仅限于现有的浏览器 API,包括 setTimeout、setImmediate 和 Mutation Observer API。 Mutation Observer uses the microtask queue 可能是引入它的原因(不确定确切的浏览器历史记录)。

在事件循环接口的可能性中,至少 Mozilla 从未实现过 setImmediate,根据 MDN,IE11 中提供了 Mutation Observers,并且在某些情况下 setTimeout 会受到限制,因此至少需要几毫秒才能完成即使延迟时间设置为零,也执行回调。

开发者竞赛

对于一个外部观察者来说,promise 库开发人员相互竞争,看谁能提出最快的时间来在 promise 解决后开始执行 promise 处理程序。

这见证了setImmediate polyfills 的引入,它根据浏览器中可用的内容选择了从事件循环中启动对蹦床的回调的最快策略。 GitHub 上的 YuzuJS / setImmediate 是这种 polyfill 的一个典型例子,它的 readme 非常值得一读。

ECMAScript 2015 采用后的历史

Promises 包含在 ES6 中,但没有指定主机实现应给予 Promise 作业的优先级。

上述YuzuJS/setImmediate polyfill 的作者也向 TC39 委员会提交了一份意见书,指定应在 ECMAScript 中给予 promise 作业高优先级。该提交最终被拒绝为不属于语言标准的实施问题。 TC39's tracking site 上没有支持提交的论据,因为它没有引用被拒绝的提案。

随后,HTML5 规范引入了在浏览器中实现 Promise 的规则。 section on how to implement ECMAScipt's EnqueueJob abstract operation in host browsers 指定它们进入微任务队列。


回答

引入微任务队列的动机是什么?为什么不继续使用任务队列来处理 Promise?

  • 微任务队列可能是为了支持 Mutation Observer 事件而引入的。

  • Promise 库的早期开发人员找到了在微任务队列中输入作业的方法,这样做可以最大限度地缩短从解决 Promise 到运行 Promise 响应作业之间的时间。这在一定程度上造成了开发人员之间的竞争,但也有助于在处理完一系列异步操作中的一个步骤后尽快继续异步程序操作。

  • 通过设计,实现和拒绝处理程序可以添加到已经解决的承诺中。如果出现这种情况,则无需等待某些事情发生,然后再继续执行 Promise 链的下一步。在这里使用微任务队列意味着下一个 Promise 处理程序是异步执行的,使用干净的堆栈,或多或少是立即的。

最终,指定微任务队列的决定是由著名的开发人员和公司根据他们的专家意见做出的。虽然这可能是一个很好的选择,但这样做的绝对必要性是没有实际意义的。


另请参阅 MDN 上的 Using microtasks in JavaScript with queueMicrotask()。

【讨论】:

  • 优秀的文章!太糟糕了,我们找不到那个被拒绝的提案。我认为至少应该将演示文稿记录在会议记录中,即使提案不再列在任何地方。 (考虑到时间,它可能没有存储库)
  • queueMicrotask() 最初是作为 global.asap 引入的,用于在archives.ecma-international.org/2014/TC39/tc39-2014-051.pdf 中对微任务(Domenic 和 Brian)进行排队?
  • @Tyrael 该链接引用了有关在 ECMAScript 规范中包含操作以尽快执行某些操作的可能性的讨论。术语“尽快”和“微任务”不在(例如)ECMAScript 2019 中,所以我认为这个想法被放弃了。 queueMicrotask 更有可能是在 HTML Standards 中引入的,以公开类似于 'setImmediate' 没有的功能,包括 setImmediate 作为规范中的标准化 timer,大概是由 whatwg 组而不是W3C。
  • 经过深入研究的答案!但还有一个问题……为什么 Mutation Observer Events/API 使用单独的队列而不是主任务队列?
  • @wlnirvana 我没有关于微任务队列的引入和 Mutation Observer API 之间的联系的信息——我最初的研究是几年前在编写 Promise polyfill 时进行的,以了解它们是如何工作的,当承诺文档令人遗憾时。
【解决方案2】:

我发现这篇 (https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 相对较旧的博文解释得非常好,并且还提供了一些旧浏览器版本的示例。

这个分离主要是用来

  1. 提高浏览器性能
  2. 执行顺序符合 ECMAScript 标准
  3. 将 HTML 相关任务和“工作”/微任务相关工作分开。

我认为需要这种行为来支持浏览器上的工作人员。工作人员无权访问 DOM,因此他们也必须为此提出一种新机制。

【讨论】:

  • 不,微任务与工人无关。工作线程也有自己的事件循环,在这方面它的工作方式与主线程完全一样。
  • 我知道它有自己的事件循环,但它无法访问运行时环境任务(DOM 相关事件、超时等)。它如何执行异步代码。为了在运行时环境级别的实现(浏览器或 nodejs)上支持这一点,有一个单独的队列更有意义,它对运行时环境任务队列不紧。
  • 不知道为什么将其称为“运行时环境任务队列”。主线程和工作线程都有自己的带有任务队列的事件循环,虽然只有主线程有 DOM 事件队列,但它们都有自己的消息通信、计时器和网络事件队列,以启用异步事件处理.为所有这些添加一个微任务队列并不是启用工作线程的原因。
【解决方案3】:

一个优点是实现之间可观察行为的可能差异更少。

如果未对这些队列进行分类,则在严格按照规范确定如何订购 setTimeout(..., 0) 回调与 promise.then(...) 回调时,将出现未定义的行为。

我认为将这些队列分类为微任务和“宏”任务的选择减少了由于异步竞争条件而可能出现的错误种类。

这一优势尤其吸引 JavaScript 库开发人员,他们的目标通常是生成高度优化的代码,同时保持跨引擎的一致可观察行为。

【讨论】:

    猜你喜欢
    • 2018-11-18
    • 1970-01-01
    • 1970-01-01
    • 2015-08-20
    • 2018-06-10
    • 2019-07-25
    • 2022-10-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多