前两天看一篇公众号文章,里面讲到了JS的事件循环,开篇第一题和打怪进阶的第一题“黄金题”我还都会做,但是到了打怪进阶的第二题“钻石题”,我就懵了,怎么也想不通为什么。
然后开始查阅各种资料,看 Promise A+ 规范,也没搞懂为什么。这个问题在困扰了我两天之后,在昨晚和同事的交流中,终于得到了解答。归根结底还是我自己对规范理解的不够透彻。
所以下面先跟着我一起来重新理解一下Promise A+ 规范规范吧。
❝A promise must provide a
then
method to access its current or eventual value or reason. A promise’sthen
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, withpromise
’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 respectiveonFulfilled
callbacks must execute in the order of their originating calls tothen
.- 2.2.6.2 If/when
promise
is rejected, all respectiveonRejected
callbacks must execute in the order of their originating calls tothen
.
翻译一下就是: 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 排版