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

12,270 阅读10分钟

image

前言

近一个多月没有写博客了,前阵子一个朋友问我一个关于 Promise 链式调用执行顺序的问题

image

凭借我对 Promise 源码的了解,这种问题能难住我?

image

然后我理所当然的回答错了

image

之后再次翻阅了一遍曾经手写的 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 个回调推入微任务队列,此时如果没有同步任务就会逐个取出再执行

另外还有几点需要注意:

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

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

new Promise(resolve => {
  resolve();
});

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

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

  1. 对于 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 注册的所有回调(没有),至此全部结束

参考资料

某人写的 promise