引入事件循环优先于任务队列的单独微任务队列的动机是什么?
What was the motivation for introducing a separate microtask queue which the event loop prioritises over the task queue?
我对JS中异步任务调度的理解
如果我有任何错误,请纠正我:
JS 运行时引擎代理由事件循环驱动,它收集任何用户和其他事件,将任务排队以处理每个回调。
事件循环连续运行,有以下思考过程:
- execution context stack(通常称为调用堆栈)是否为空?
- 如果是,则将微任务队列(或作业队列)中的任何微任务插入调用堆栈。继续这样做,直到微任务队列为空。
- 如果微任务队列为空,则将任务队列(或回调队列)中最早的任务插入调用堆栈
所以有两个关键区别b/w任务和微任务的处理方式:
- 微任务(例如promises use microtask queue to run their callbacks) are prioritised over tasks (e.g. callbacks from othe web APIs such as setTimeout)
- 此外,所有微任务都在任何其他事件处理或渲染或任何其他任务发生之前完成。因此,微任务之间的应用环境基本相同。
ES6 2015 中引入了 Promise。我假设微任务队列也是在 ES6 中引入的。
我的问题
引入微任务队列的动机是什么?为什么不继续使用任务队列来实现承诺?
更新 #1 - 我正在寻找对规范进行此更改的明确历史原因 - 即它旨在解决的问题是什么,而不是关于微任务的好处的自以为是的答案排队。
参考文献:
- In depth: Microtasks and the JavaScript runtime environment
- HTML spec event loop processing model
- Javascript-hard-parts-v2
- loupe - 理解 JavaScript 调用的可视化工具 stack/event loop/callback 队列交互
- Using microtasks in JavaScript with queueMicrotask()
一个优点是实现之间可观察行为的可能差异较小。
如果不对这些队列进行分类,那么在确定如何严格按照规范对 setTimeout(..., 0)
回调与 promise.then(...)
回调进行排序时,将会出现未定义的行为。
我认为将这些队列分类为微任务和“宏”任务的选择减少了由于异步竞争条件可能导致的错误类型。
此优势尤其吸引 JavaScript 库开发人员,他们的目标通常是生成高度优化的代码,同时保持跨引擎的一致可观察行为。
我发现这个 (https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 相对较旧的博客 post 解释得非常好,它还提供了一些关于旧浏览器版本的示例。
这个分离主要是用来
- 提高浏览器性能
- 执行顺序符合ECMAScript标准
- 将 HTML 相关任务和 'job'/微任务相关工作分开。
我认为需要这种行为来支持浏览器上的工作人员。工作人员无法访问 DOM,因此他们也必须为此想出一种新机制。
Promises were introduced in ES6 2015. I assume the microtask queue was also introduced in ES6.
实际上 ECMAScript 标准根本没有引入微任务任务队列:ES6 标准指定将承诺处理作业放入 TriggerPromiseReactions, using the abstract process EnqueueJob 下名为“PromiseJobs”的队列中,以将作业输入一个主机环境实现的作业队列,没有规定主机队列应该如何处理。
在被 ECMAScript 采用之前
Promise 库是在用户领域开发的。执行 promise 处理程序、监视它们是否抛出或返回一个值并可以访问 promise 链中下一个 promise 的 resolve
和 reject
函数的代码位称为“蹦床”。虽然是 Promise 库的一部分,但 trampoline 不被视为用户代码的一部分,并且使用干净堆栈调用 promise 处理程序的声明排除了被 trampoline 占用的堆栈 space。
如果尚未 运行 宁,则需要启动蹦床以 运行 承诺作业,并使用处理程序列表来调用已确定状态(已完成或已拒绝)的承诺的结算。
以空堆栈开始蹦床执行的方法仅限于现有的浏览器 API,包括 setTimeout
、setImmediate
和 Mutation Observer API. The Mutation Observer uses the microtask queue,这可能是引入它的原因(不是确定确切的浏览器历史记录)。
在事件循环接口的可能性中,setImmediate
至少从未被 Mozilla 实现,根据 MDN,IE11 中提供了 Mutation Observers,并且 setTimeout
在某些情况下会受到限制,因此它会即使延迟时间设置为零,也至少需要几毫秒才能执行回调。
开发者大赛
对外部观察者而言,Promise 库的开发人员相互竞争,看谁能想出最快的时间在 promise 结算后开始执行 promise 处理程序。
这看到了 setImmediate
polyfills 的引入,它根据浏览器中可用的内容选择了从事件循环开始回调到 trampoline 的最快策略。 柚子/
GitHub 上的 setImmediate 是此类 polyfill 的主要示例,其 readme
非常值得一读。
历史 在 ECMAScript 2015 中采用后
承诺包含在 ES6 中,但没有指定主机实现应给予承诺作业的优先级。
YuzuJS/setImmediate polyfill above also made a submission to the TC39 committee to specify that promise jobs should be given high priority in ECMAScript. The submission was ultimately rejected as an implementation issue not belonging to the language standard. Arguments supporting the submission are unavailable on TC39's tracking site 的作者,因为它没有引用被拒绝的提案。
随后 HTML5 规范引入了浏览器中 Promise 实现的规则。 section on how to implement ECMAScipt's EnqueueJob abstract operation in host browsers 指定它们进入微任务队列。
回答
What was the motivation for introducing the microtask queue? Why not just keep using the task queue for promises as well?
引入微任务队列以支持 Mutation Observer Events,正如 Jake Archibald 在 JSConf Asia 2018 (24:07)1 期间所详述的他的“In the loop”演示文稿。
promise 库的早期开发人员找到了在微任务队列中输入作业的方法,这样做最大限度地减少了确定 promises 和 运行 完成 promise 反应作业之间的时间。这在某种程度上造成了开发人员之间的竞争,但也有助于在处理完一系列异步操作中的一个步骤后尽快继续异步程序操作。
通过设计,可以将履行和拒绝处理程序添加到已经结算的承诺中。如果出现这种情况,则无需等待某事发生就可以继续执行承诺链的下一步。在这里使用微任务队列意味着下一个 promise 处理程序是异步执行的,具有干净的堆栈,或多或少是立即执行的。
最终,指定微任务队列的决定是由杰出的开发人员和公司根据他们的专家意见做出的。虽然这可能是一个很好的选择,但这样做的绝对必要性尚无定论。
另请参阅 MDN 上的 Using microtasks in JavaScript with queueMicrotask()。
1 感谢@Minh Nghĩa 对 Jake Archibald 的“In the Loop”(0:00) 演讲的 link 评论 - ☆☆ ☆☆☆。亮点包括
- 事件循环一次执行任务队列中的一个任务,动画队列中的所有任务(执行队列时添加的任务除外),以及微任务队列中的所有任务,直到它为空。
- 依赖于棘手的事件处理程序和承诺回调的执行顺序可能会导致单元测试失败,因为以编程方式分派的事件是同步执行事件处理程序的,而不是通过事件循环。
我对JS中异步任务调度的理解
如果我有任何错误,请纠正我:
JS 运行时引擎代理由事件循环驱动,它收集任何用户和其他事件,将任务排队以处理每个回调。
事件循环连续运行,有以下思考过程:
- execution context stack(通常称为调用堆栈)是否为空?
- 如果是,则将微任务队列(或作业队列)中的任何微任务插入调用堆栈。继续这样做,直到微任务队列为空。
- 如果微任务队列为空,则将任务队列(或回调队列)中最早的任务插入调用堆栈
所以有两个关键区别b/w任务和微任务的处理方式:
- 微任务(例如promises use microtask queue to run their callbacks) are prioritised over tasks (e.g. callbacks from othe web APIs such as setTimeout)
- 此外,所有微任务都在任何其他事件处理或渲染或任何其他任务发生之前完成。因此,微任务之间的应用环境基本相同。
ES6 2015 中引入了 Promise。我假设微任务队列也是在 ES6 中引入的。
我的问题
引入微任务队列的动机是什么?为什么不继续使用任务队列来实现承诺?
更新 #1 - 我正在寻找对规范进行此更改的明确历史原因 - 即它旨在解决的问题是什么,而不是关于微任务的好处的自以为是的答案排队。
参考文献:
- In depth: Microtasks and the JavaScript runtime environment
- HTML spec event loop processing model
- Javascript-hard-parts-v2
- loupe - 理解 JavaScript 调用的可视化工具 stack/event loop/callback 队列交互
- Using microtasks in JavaScript with queueMicrotask()
一个优点是实现之间可观察行为的可能差异较小。
如果不对这些队列进行分类,那么在确定如何严格按照规范对 setTimeout(..., 0)
回调与 promise.then(...)
回调进行排序时,将会出现未定义的行为。
我认为将这些队列分类为微任务和“宏”任务的选择减少了由于异步竞争条件可能导致的错误类型。
此优势尤其吸引 JavaScript 库开发人员,他们的目标通常是生成高度优化的代码,同时保持跨引擎的一致可观察行为。
我发现这个 (https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 相对较旧的博客 post 解释得非常好,它还提供了一些关于旧浏览器版本的示例。
这个分离主要是用来
- 提高浏览器性能
- 执行顺序符合ECMAScript标准
- 将 HTML 相关任务和 'job'/微任务相关工作分开。
我认为需要这种行为来支持浏览器上的工作人员。工作人员无法访问 DOM,因此他们也必须为此想出一种新机制。
Promises were introduced in ES6 2015. I assume the microtask queue was also introduced in ES6.
实际上 ECMAScript 标准根本没有引入微任务任务队列:ES6 标准指定将承诺处理作业放入 TriggerPromiseReactions, using the abstract process EnqueueJob 下名为“PromiseJobs”的队列中,以将作业输入一个主机环境实现的作业队列,没有规定主机队列应该如何处理。
在被 ECMAScript 采用之前
Promise 库是在用户领域开发的。执行 promise 处理程序、监视它们是否抛出或返回一个值并可以访问 promise 链中下一个 promise 的 resolve
和 reject
函数的代码位称为“蹦床”。虽然是 Promise 库的一部分,但 trampoline 不被视为用户代码的一部分,并且使用干净堆栈调用 promise 处理程序的声明排除了被 trampoline 占用的堆栈 space。
如果尚未 运行 宁,则需要启动蹦床以 运行 承诺作业,并使用处理程序列表来调用已确定状态(已完成或已拒绝)的承诺的结算。
以空堆栈开始蹦床执行的方法仅限于现有的浏览器 API,包括 setTimeout
、setImmediate
和 Mutation Observer API. The Mutation Observer uses the microtask queue,这可能是引入它的原因(不是确定确切的浏览器历史记录)。
在事件循环接口的可能性中,setImmediate
至少从未被 Mozilla 实现,根据 MDN,IE11 中提供了 Mutation Observers,并且 setTimeout
在某些情况下会受到限制,因此它会即使延迟时间设置为零,也至少需要几毫秒才能执行回调。
开发者大赛
对外部观察者而言,Promise 库的开发人员相互竞争,看谁能想出最快的时间在 promise 结算后开始执行 promise 处理程序。
这看到了 setImmediate
polyfills 的引入,它根据浏览器中可用的内容选择了从事件循环开始回调到 trampoline 的最快策略。 柚子/
GitHub 上的 setImmediate 是此类 polyfill 的主要示例,其 readme
非常值得一读。
历史 在 ECMAScript 2015 中采用后
承诺包含在 ES6 中,但没有指定主机实现应给予承诺作业的优先级。
YuzuJS/setImmediate polyfill above also made a submission to the TC39 committee to specify that promise jobs should be given high priority in ECMAScript. The submission was ultimately rejected as an implementation issue not belonging to the language standard. Arguments supporting the submission are unavailable on TC39's tracking site 的作者,因为它没有引用被拒绝的提案。
随后 HTML5 规范引入了浏览器中 Promise 实现的规则。 section on how to implement ECMAScipt's EnqueueJob abstract operation in host browsers 指定它们进入微任务队列。
回答
What was the motivation for introducing the microtask queue? Why not just keep using the task queue for promises as well?
引入微任务队列以支持 Mutation Observer Events,正如 Jake Archibald 在 JSConf Asia 2018 (24:07)1 期间所详述的他的“In the loop”演示文稿。
promise 库的早期开发人员找到了在微任务队列中输入作业的方法,这样做最大限度地减少了确定 promises 和 运行 完成 promise 反应作业之间的时间。这在某种程度上造成了开发人员之间的竞争,但也有助于在处理完一系列异步操作中的一个步骤后尽快继续异步程序操作。
通过设计,可以将履行和拒绝处理程序添加到已经结算的承诺中。如果出现这种情况,则无需等待某事发生就可以继续执行承诺链的下一步。在这里使用微任务队列意味着下一个 promise 处理程序是异步执行的,具有干净的堆栈,或多或少是立即执行的。
最终,指定微任务队列的决定是由杰出的开发人员和公司根据他们的专家意见做出的。虽然这可能是一个很好的选择,但这样做的绝对必要性尚无定论。
另请参阅 MDN 上的 Using microtasks in JavaScript with queueMicrotask()。
1 感谢@Minh Nghĩa 对 Jake Archibald 的“In the Loop”(0:00) 演讲的 link 评论 - ☆☆ ☆☆☆。亮点包括
- 事件循环一次执行任务队列中的一个任务,动画队列中的所有任务(执行队列时添加的任务除外),以及微任务队列中的所有任务,直到它为空。
- 依赖于棘手的事件处理程序和承诺回调的执行顺序可能会导致单元测试失败,因为以编程方式分派的事件是同步执行事件处理程序的,而不是通过事件循环。