2021 年 12 月 12 日

微任务

Promise 处理程序 .then/.catch/.finally 始终是异步的。

即使 Promise 立即得到解决,位于 .then/.catch/.finally 下面的代码行仍然会先于这些处理程序执行。

这是一个演示

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // this alert shows first

如果你运行它,你首先会看到 代码已完成,然后是 promise 完成!

这很奇怪,因为这个 promise 肯定是从一开始就完成的。

为什么 .then 会在之后触发?发生了什么事?

微任务队列

异步任务需要适当的管理。为此,ECMA 标准指定了一个内部队列 PromiseJobs,更常称为“微任务队列”(V8 术语)。

规范 中所述

  • 队列遵循先进先出原则:首先排队的任务首先运行。
  • 只有当没有其他任务正在运行时,才会开始执行任务。

或者,更简单地说,当一个 Promise 准备就绪时,它的 .then/catch/finally 处理程序将被放入队列中;它们尚未执行。当 JavaScript 引擎从当前代码中释放出来时,它将从队列中获取一个任务并执行它。

这就是为什么上面的示例中“代码已完成”首先显示的原因。

Promise 处理程序始终会通过此内部队列。

如果存在具有多个 .then/catch/finally 的链,那么其中的每一个都将异步执行。也就是说,它首先进入队列,然后在当前代码完成后并先前排队的处理程序完成后执行。

如果顺序对我们很重要怎么办?我们如何使 code finishedpromise done 之后出现?

很简单,只需使用 .then 将其放入队列中

Promise.resolve()
  .then(() => alert("promise done!"))
  .then(() => alert("code finished"));

现在顺序按预期的那样了。

未处理的拒绝

还记得文章 使用 Promise 进行错误处理 中的 unhandledrejection 事件吗?

现在我们可以确切地看到 JavaScript 如何发现存在未处理的拒绝。

当 Promise 错误未在微任务队列的末尾处理时,就会发生“未处理的拒绝”。

通常,如果我们预期出现错误,我们会向 Promise 链添加 .catch 来处理它

let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));

// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));

但是,如果我们忘记添加 .catch,那么在微任务队列为空之后,引擎将触发该事件

let promise = Promise.reject(new Error("Promise Failed!"));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

如果我们稍后处理错误怎么办?像这样

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

现在,如果我们运行它,我们将首先看到 Promise Failed!,然后是 caught

如果我们不知道微任务队列,我们可能会想:“为什么 unhandledrejection 处理程序运行?我们确实捕获并处理了错误!”

但现在我们理解了 unhandledrejection 是在微任务队列完成后生成的:引擎检查 Promise,如果其中任何一个处于“拒绝”状态,则触发该事件。

在上面的示例中,由 setTimeout 添加的 .catch 也被触发。但它稍后才这样做,在 unhandledrejection 已经发生之后,所以它不会改变任何东西。

总结

Promise 处理始终是异步的,因为所有 Promise 操作都通过内部“Promise 作业”队列(也称为“微任务队列”(V8 术语))传递。

因此,.then/catch/finally 处理程序总是在当前代码完成后才被调用。

如果我们需要确保一段代码在 .then/catch/finally 之后执行,我们可以将其添加到一个链式 .then 调用中。

在大多数 JavaScript 引擎(包括浏览器和 Node.js)中,微任务的概念与“事件循环”和“宏任务”紧密相关。由于它们与 Promise 没有直接关系,因此将在本教程的另一部分《事件循环:微任务和宏任务》中进行介绍。

教程地图

评论

评论前请阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请用 <pre> 标签将它们包装起来,对于超过 10 行的代码,请使用沙箱(plnkrjsbincodepen…)