Promise链式调用顺序引发的思考

深度揭秘 Promise 微任务注册和执行过程

有道题,得细说(一道异步相关的面试题)

问题

题目是这样的,为了更加语义化我将打印的字符串做了一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 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 中的回调是异步的

1
2
3
4
5
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 注册的所有回调,考虑以下案例

1
2
3
4
5
6
7
8
9
10
11
12
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 内部储存起来,等到 p 被 resolve 后,依次将这 3 个回调推入微任务队列,此时如果没有同步任务就会逐个取出再执行

另外还有几点需要注意:

  1. 对于普通的 promise 来说,当执行完 resolve 函数时,promise 状态就为 resolved

而 resolve 函数就是在实例化 Promise 时,传入函数的第一个参数

1
2
3
new Promise(resolve ={
resolve();
});

它的作用除了将当前的 promise 由 pending 变为 resolved,还会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中,很多人以为是由 then 方法来触发它保存回调,而事实上是由 promise 的 resolve 来触发的,then 方法只负责注册回调

具体的行为可以参考底部链接

  1. 对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve

同步返回值的意思换句话说,如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后再 resolve(这句话有点像绕口令哈哈哈~)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise((resolve, reject) => {
resolve();
})
.then(() =>
new Promise((resolve, reject) => {
resolve();
}).then(() => {
console.log("log: 内部第一个then");
})
)
.then(() => {
console.log("log: 外部第二个then");
});

// log: 内部第一个then
// log: 外部第二个then

这里外部的第一个 then 的回调返回了一个 promise,所以外部第一个 then 返回的 promise 需要等到内部整个 promise (红框) 被 resolve 后才会被 resolve

image

当打印 log: 内部第一个then 后,回调执行完毕,蓝框的 promise 被 resolve,然后外部第一个 then 返回的 promise 才被 resolve

随后遍历之前通过 then 给外部第一个 then 返回的 promise 注册的所有回调(黄框),放入微任务队列,等同步任务执行完毕后,依次取出执行,最终打印 log: 外部第二个then

解析

分析完 promise 和 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 注册的所有回调(没有),至此全部结束.