前言
近一个多月没有写博客了,前阵子一个朋友问我一个关于 Promise 链式调用执行顺序的问题
凭借我对 Promise 源码的了解,这种问题能难住我?
然后我理所当然的回答错了
之后再次翻阅了一遍曾经手写的 Promise,理清了其中的缘由,写下这篇文章,希望对 Promise 有更深一层的理解
问题
题目是这样的,为了更加语义化我将打印的字符串做了一些修改
new Promise((resolve, reject) => {
console.log("log: 外部promise");
resolve();
})
.then(() => {
console.log("log: 外部第一个then");
new Promise((resolve, reject) => {
console.log("log: 内部promise");
resolve();
})
.then(() => {
console.log("log: 内部第一个then");
})
.then(() => {
console.log("log: 内部第二个then");
});
})
.then(() => {
console.log("log: 外部第二个then");
});
// log: 外部promise
// log: 外部第一个then
// log: 内部promise
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then
它的考点并不仅限于 Promise 本身,同时还考察 Promise 链式调用之间的执行顺序,在开始解析之前,首先要清楚 Promise 能够链式调用的原理,即
promise 的 then/catch 方法执行后会也返回一个 promise
这里先抛出结论,然后再对题目进行解析
结论1
当执行 then 方法时,如果前面的 promise 已经是 resolved 状态,则直接将回调放入微任务队列中
执行 then 方法是同步的,而 then 中的回调是异步的
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.log("log: 外部第一个then");
});
实例化 Promise 传入的函数是同步执行的,then 方法本身其实也是同步执行的,但 then 中的回调会先放入微任务队列,等同步任务执行完毕后,再依次取出执行,换句话说只有回调是异步的
同时在同步执行 then 方法时,会进行判断:
- 如果前面的 promise 已经是 resolved 状态,则会立即将回调推入微任务队列(但是执行回调还是要等到所有同步任务都结束后)
- 如果前面的 promise 是 pending 状态则会将回调存储在 promise 的内部,一直等到 promise 被 resolve 才将回调推入微任务队列
结论2
当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中
如何理解通过 then 给这个 promise 注册的所有回调
,考虑以下案例
let p = new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
p.then(() => {
console.log("log: 外部第一个then");
});
p.then(() => {
console.log("log: 外部第二个then");
});
p.then(() => {
console.log("log: 外部第三个then");
});
1 秒后变量 p 才会被 resolve,但是在 resolve 前通过 then 方法给它注册了 3 个回调,此时这 3 个回调不会被执行,也不会被放入微任务队列中,它们会被 p 内部储存起来(在手写 promise 时,这些回调会放在 promise 内部保存的数组中),等到 p 被 resolve 后,依次将这 3 个回调推入微任务队列,此时如果没有同步任务就会逐个取出再执行
另外还有几点需要注意:
- 对于普通的 promise 来说,当执行完 resolve 函数时,promise 状态就为 resolved
resolve 函数就是在实例化 Promise 时,传入函数的第一个参数
new Promise(resolve => {
resolve();
});
它的作用除了将当前的 promise 由 pending 变为 resolved,还会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中,很多人以为是由 then 方法来触发它保存回调,而事实上 then 方法即不会触发回调,也不会将它放到微任务,then 只负责注册回调,由 resolve 将注册的回调放入微任务队列,由事件循环将其取出并执行
具体的行为可以参考底部链接
- 对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve
同步返回值
的意思换句话说
如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后,再往微任务队列推入一个任务,而这个任务的作用是 resolve 包裹这个回调的 then 方法返回的 promise
若回调的返回值非 promise ,则直接 resolve,不会有前面的额外逻辑
来看以下例子:
new Promise(resolve => {
resolve();
})
.then(() => {
new Promise(resolve => {
resolve();
})
.then(() => {
console.log("log: 内部第一个then");
return Promise.resolve();
})
.then(() => console.log("log: 内部第二个then"));
})
.then(() => console.log("log: 外部第二个then"));
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then
这里第一个 then 方法返回了一个 promise(绿框),根据前面第二条的结论,当绿框的 promise 被 resolve 时,黄框中的回调就会被推入微任务队列,接着开始执行绿框的回调
主线程:绿框回调
微任务队列:空
绿框中的回调里声明了一个 promise,之后调用了 then 方法(紫框),由于前面的 promise 已经是 resolved 状态,所以紫框中的回调会立即被推入微任务队列
主线程:绿框回调
微任务队列:紫框 promise 的回调
接着执行到第二个 then 方法(红框),由于紫框中的回调还在微任务队列没执行,所以紫框还是 pending 状态,所以红框中的回调会被注册,等到紫框被 resolve 时,才推入微任务队列
主线程:绿框回调
微任务队列:紫框 promise 的回调
绿框中的回调的同步逻辑全部执行完毕,返回值是 undefined,此时绿框的 promise 被 resolve,黄框的 promise 随即推入微任务
主线程:空
微任务队列:紫框 promise 的回调,黄框 promise 的回调
主线程空闲,依次取出微任务队列的任务执行
主线程:紫框 promise 的回调
微任务队列:黄框 promise 的回调
接下来是重点,紫框 promise 的回调执行完毕后,打印 log: 内部第一个 then
,同时返回了一个 resolved 状态的 promise,此时紫框 promise 不会被立即 resolve,取而代之往微任务队列中推入一个额外的微任务,当这个微任务执行的时,紫框 promise 才会被 resolve
如果对这个额外对微任务还有疑惑,可以看底部源码的这一部分
主线程:空
微任务队列:黄框 promise 的回调,resolve 紫框 promise 的任务
此时主线程再次空闲,取出微任务队列中的第一个任务执行,即黄框 promise 的回调,打印 log: 外部第二个 then
主线程:空
微任务队列:resolve 紫框 promise 的任务
同上,取出这个额外的任务并执行,此时紫框的 promise 才算被 resolve。一旦紫框的 promise 被 resolve,之前注册的红框 promise 的回调就会被推入微任务队列
主线程:空
微任务队列:红框 promise 的回调
接着取出任务,执行红框 promise 回调,打印 log: 内部第二个 then
,至此全部结束
解析问题
难倒我的那个问题没有上面那个问题那么复杂,接下来我们来结合上面的两个结论来分析这个问题(建议分屏,比对问题章节中的案例代码查看解析)
首先 Promise 实例化时,同步执行函数,打印 log: 外部promise
,然后执行 resolve 函数,将 promise 变为 resolved,但由于此时 then 方法还未执行,所以遍历所有 then 方法注册的回调时什么也不会发生(结论2第一条)
此时剩余任务如下:
主线程:外部第一个 then,外部第二个 then
微任务队列:空
接着执行外部第一个 then(以下简称:外1then),由于前面的 promise 已经被 resolve,所以立即将回调放入微任务队列(结论1)
主线程:外2then
微任务队列:外1then 的回调
但是由于此时这个回调还未执行,所以外1then 返回的 promise 仍为 pending 状态(结论2第二条),继续同步执行外2then,由于前面的 promise 是 pending 状态,所以外2then 的回调也不会被推入微任务队列也不会执行(结论2案例)
主线程:空
微任务队列:外1then 的回调
当主线程执行完毕后,执行微任务,也就是外1then 的回调,回调中首先打印log: 外部第一个then
随后实例化内部 promise,在实例化时执行函数,打印 log: 内部promise
,然后执行 resolve 函数(结论1),接着执行到内部的第一个 then(以下简称:内1then),由于前面的 promise 已被 resolve,所以将回调放入微任务队列中(结论1)
主线程:内2then
微任务队列:内1then 的回调
由于正在执行外1then 的回调,所以外1then 返回的 promise 仍是 pending 状态,外2then 的回调仍不会被注册也不会被执行
接着同步执行内2then,由于它前面的 promise (内1then 返回的 promise) 是 pending 状态(因为内1then 的回调在微任务队列中,还未执行),所以内2then 的回调和外2then 的回调一样,不注册不执行(结论2案例)
主线程:空
微任务队列:内1then 的回调
此时外1then 的回调全部执行完毕,外1then 返回的 promise 的状态由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们的回调放入微任务队列中(结论2),即放入外2then 的回调
主线程:空
微任务队列:内1then 的回调,外2then 的回调
此时主线程逻辑执行完毕,取出第一个微任务执行
主线程:内1then 的回调
微任务队列:外2then 的回调
执行内1then 的回调打印 log: 内部第一个then
,回调执行完毕后,内1then 返回的 promise 由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们的回调放入微任务队列中(结论2),即放入内2then 的回调
主线程:空
微任务队列:外2then 的回调,内2then 的回调
执行外2then 的回调打印 log: 外部第二个then
,回调执行完毕,外2then 返回的 promise 由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们放入微任务队列中(结论2)
这时由于外2 then 返回的 promise 没有再进一步的链式调用了,主线程任务结束
主线程:空
微任务队列:内2then 的回调
接着取出微任务,执行内2then 的回调打印 log: 内部第二个then
,内2then 返回的 promise 的状态变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调(没有),至此全部结束