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

8,253 阅读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时,onFulfilledonRejected回调函数的调用顺序将会按照在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 排版