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 finished
在 promise 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 没有直接关系,因此将在本教程的另一部分《事件循环:微任务和宏任务》中进行介绍。
评论
<code>
标签,对于多行代码,请用<pre>
标签将它们包装起来,对于超过 10 行的代码,请使用沙箱(plnkr、jsbin、codepen…)