彻底理解Promise.then回调的执行顺序

9,788 阅读5分钟

前两天看一篇公众号文章,里面讲到了JS的事件循环,开篇第一题和打怪进阶的第一题“黄金题”我还都会做,但是到了打怪进阶的第二题“钻石题”,我就懵了,怎么也想不通为什么。

然后开始查阅各种资料,看 Promise A+ 规范,也没搞懂为什么。这个问题在困扰了我两天之后,在昨晚和同事的交流中,终于得到了解答。归根结底还是我自己对规范理解的不够透彻。

所以下面先跟着我一起来重新理解一下Promise A+ 规范规范吧。

❝

A promise must provide a then method to access its current or eventual value or reason. A promise’s then method accepts two arguments:

promise.then(onFulfilled, onRejected)

  • 2.2.2 If onFulfilled is a function:
    • 2.2.2.1 it must be called after promise is fulfilled, with promise’s value as its first argument.
    • 2.2.2.2 it must not be called before promise is fulfilled.
    • 2.2.2.3 it must not be called more than once.
❞

翻译一下就是: promise必须提供一个then方法来存取它当前或最终的值或者原因。promise的then方法接收两个参数:

promise.then(onFulfilled, onRejected)

如果 onFulfilled 是函数,「此函数必须在promise 完成(fulfilled)后被调用,并把promise 的值作为它的第一个参数」;此函数在promise完成(fulfilled)之前绝对不能被调用;此函数绝对不能被调用超过一次。

❝
  • 2.2.6 then may be called multiple times on the same promise.
    • 2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then.
    • 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
❞

翻译一下就是: then 在同一个promise里可以被调用多次,并且当promise的状态变为fulfilled或者rejected时,onFulfilled和onRejected回调函数的调用顺序将会按照在then里定义的顺序进行调用。

也就是像下面的代码那样:

const promise1 = new Promise((resolve, reject) => {
 console.log(1);
  resolve();
});

promise1.then(() => {
 console.log(2);
});

promise1.then(() => {
 console.log(3);
})
❝
  • 2.2.7 then must return a promise.
❞

翻译一下就是: then 必须返回一个promise。也是因为这个规范,所以 promise 支持「链式调用」。

也就是像下面的代码那样:

const promise1 = new Promise((resolve, reject) => {
 console.log(1);
  resolve();
});

const promise2 = promise1.then(() => {
 console.log(2);
});

const promise3 = promise2.then(() => {
 console.log(3);
})

// or
new Promise((resolve, reject) => {
 console.log(1);
  resolve();
})
.then(() => {
 console.log(2);
})
.then(() => {
 console.log(3);
});

上面的代码打印顺序就是1,2,3。下面我们对代码来做一点改动:

new Promise((resolve, reject) => {
 console.log(1);
  resolve();
})
.then((a) => {
 console.log(2);
  new Promise((resolve,reject) => {
   console.log(3);
    resolve();
  })
  .then((c) => {
   console.log(4);
  })
  .then((d) => {
   console.log(6);
  })
})
.then((b) => {
 console.log(5);
});

ok,结合前面理解的几条Promise A+规范,让我们来一起分析一下这段代码的执行顺序:

  • 首先打印 1 ,后面跟着个resolve调用,说明 promise 的状态变为fulfilled,所以把它的下一个 then 的回调即 「a回调」 放入微任务队列等待执行;根据规范2.2.7,then必须返回一个promise ,因此包含「a回调」的这个then返回了一个新的promise,我们记为promise1 ,注意此时「a回调」还没被执行,也就是这个promise1的状态还是pending;根据规范2.2.2.1,then回调的执行必须在上一个promise的状态为fulfilled,所以下一个then,也就是「b回调」其实是被缓存在promise1内部的回调队列里,等promise1的状态改变再放入微任务队列。
  • 接着执行「a回调」,先打印2,然后接着往下执行,遇到了一个新的 promise,我们记为promise2, 接着先打印3,然后后面跟着个resolve调用,说明这个promise2的状态变为fulfilled,所以把它的下一个 then 的回调也就是「c回调」放入微任务队列等待执行,同样根据规范2.2.7,包含c回调的这个then也返回了一个新的promise,我们记为promise3,此时c回调还没有执行,也就是这个promise3的状态还是pending,同样根据规范2.2.2.1,所以下一个then,也就是「d回调」其实是被缓存在promise3内部的回调队列里,等promise3的状态改变再放入微任务队列。
  • 接着「a回调」执行完了,没有返回东西,可以理解为返回undefined ,根据规范2.3.4,如果 x 既不是对象也不是函数,用x完成(fulfill)promise,说明上面的promise1的状态变为了fulfilled,因此之前的「b回调」此时可以被放入微任务队列里等待执行了。
  • 经过上面的步骤,此时微任务队列里存在「c回调」和「b回调」。
  • 接着先执行「c回调」,打印4,「c回调」执行完成没有问题,根据规范2.3.4,也就是上面说到的promise3的状态变为了fulfilled,此时「d回调」可以被放入微任务队列等待执行了。
  • 接着执行「b回调」,打印5。
  • 接着执行「d回调」,打印6。

综上所述,打印顺序为1,2,3,4,5,6。

理解了上面的执行顺序,我们再来稍微改变一下上面的代码,我们给内部的这个promise添加一个return,来看看打印顺序会不会发生改变。

new Promise((resolve, reject) => {
 console.log(1);
  resolve();
})
.then((a) => {
 console.log(2);
  return new Promise((resolve,reject) => {
   console.log(3);
    resolve();
  })
  .then((c) => {
   console.log(4);
  })
  .then((d) => {
   console.log(6);
  })
})
.then((b) => {
 console.log(5);
});

根据规范2.3.2,如果 x 是一个promise,采用promise的状态,如果 x 是请求状态(pending), promise (也就是这个then代表的promise)必须保持pending直到 x fulfilled 或 rejected;如果 x 是完成态(fulfilled),用相同的值完成fulfill promise ;如果 x 是拒绝态(rejected),用相同的原因reject promise 。

我的理解:此时「包含a回调的这个then」返回了一个新的promise,再链式调用的话,相当于「包含b回调的这个then」是被跟在返回的这个新的promise上。因此上面的代码可以直接理解为下面的代码:

new Promise((resolve, reject) => {
 console.log(1);
  resolve();
})
.then((a) => {
 console.log(2);
  return new Promise((resolve,reject) => {
   console.log(3);
    resolve();
  })
  .then((c) => {
   console.log(4);
  })
  .then((d) => {
   console.log(6);
  })
  .then((b) => {
    console.log(5);
  });
})

换成这段代码,再让你说出输出顺序就没问题了吧?答案是1,2,3,4,6,5。

打个广告:

欢迎关注微信公众号「前端不慌张」,让你的前端之路不再慌张。

参考文章:

本文使用 mdnice 排版