遇到一个非常离谱的求输出顺序题,本以为能轻松秒杀,没想到直接来个大的,各种混杂非常烧脑。具体题目代码放在末尾,有兴趣的可以直接挑战一下。这里截取一个核心考点:Promise.then 的执行顺序。
问题
先来看一个问题吧,以下代码的输出顺序是:
code snippetCopyjavascriptnew 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)
})
答案
code snippetCopytext1 -> 2 -> 3 -> 4 -> 5 -> 6
我们知道 Promise 产生的任务为微任务,需要添加到微任务队列。那么问题来了:在输出 3 后轮到谁添加到队列?是输出 5 还是输出 4?
Promise/A+ 规范
要理解其中的过程,我们需要了解一下什么是Promise/A+规范。摘取比较重要的几点:
1、一个 Promise 的当前状态必须为以下三种状态中的一种。
等待态(Pending)、完成态(Fulfilled)和拒绝态(Rejected)。
2、一个 Promise 必须提供一个 then 方法以访问其当前值、终值和拒绝原因。
code snippetCopyjavascript// 提供两个参数,都为函数类型
promise.then(onFulfilled, onRejected)
如果 onFulfilled 是函数,此函数必须在 Promise 完成后被调用,并把 Promise 的值作为它的第一个参数;此函数在 Promise 完成态之前绝对不能被调用;此函数绝对不能被调用超过一次。
3、then 必须返回一个 Promise,并且在同一个 Promise 里可以被调用多次。
当 Promise 的状态变为完成态或者拒绝态时,onFulfilled 和 onRejected 回调函数的调用顺序将会按照在 then 里定义的顺序进行调用(也就是大家所熟知的 then 链式调用):
code snippetCopyjavascriptnew Promise((resolve, reject) => {
console.log(1)
resolve()
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
解决
结合前面理解的几条 Promise/A+ 规范,让我们来一起分析一下这段代码的执行顺序。
前面的输出容易理解:首先打印 1,之后调用了 resolve()
函数,这将首个 Promise(记为 P0)的状态更改为完成态。这时将后面跟紧的 then()
放入微任务队列(打印 2 等,记为 P1),这时可能就会有疑问了:那最后的 then(打印 5,记为 P2)需要紧跟着进入微任务队列吗?这时我们必须结合规范了。
- 根据规范2.2.7,then 必须返回一个 Promise(也就是上面的 P1)。注意它被放入队列后回调还没被执行,也就是状态还是等待态。
- 根据规范2.2.2.1,then 回调的执行必须在上一个 Promise 的状态为 fulfilled,所以下一个紧跟的 then(P2)其实是被缓存在 P1 的内回调队列里,等这个 Promise 的状态改变再放入微任务队列。
此时的微任务队列:
code snippetCopytext|------|------|------|------|------| | P1 | | | | | |------|------|------|------|------|
接着执行微任务队列的第一个微任务(P1),即打印 2,然后接着往下执行,遇到了一个新的 Promise,我们记为 P1-0, 接着先打印 3。后面调用了 resolve()
使得 P1-0 变为完成态,之后便将后面的 then (打印 4,记为 P1-1)添加到微任务队列中。同样的道理,之后的 then(打印 6,记为 P1-2)被缓存在 P1-1 中。
此时的微任务队列:
code snippetCopytext|------|------|------|------|------| | P1 | P1-1 | | | | |------|------|------|------|------|
接着 P1 执行完了,没有返回东西,可以理解为返回 undefined ,根据规范2.3.4,如果 x 既不是对象也不是函数,用 x 完成(fulfill)Promise,说明上面的 P1 的状态变为了完成态,因此之前的 P2 此时可以被放入微任务队列里等待执行了。
此时的微任务队列:
code snippetCopytext|------|------|------|------|------| | P1-1 | P2 | | | | |------|------|------|------|------|
之后运行 P1-1 微任务,打印 4。注意它后面有一个 then(P1-2),P1-1 变为完成态后将 P1-2 加入了微任务队列。
此时的微任务队列:
code snippetCopytext|------|------|------|------|------| | P2 | P1-2 | | | | |------|------|------|------|------|
因为没有额外的 then 分析了,微任务队列内的任务依次执行。所以输出结果就是,P2 -> 5,P1-2 -> 6。
扩展
我们知道,链式调用的 then 的值是上一个 then 的返回值或者 resolve()
传递的值,如果 then 返回一个新的 Promise 会怎样呢?下面是引题的变体:
code snippetCopyjavascriptnew 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 是请求状态, Promise (也就是这个 then 代表的 Promise)必须保持请求态直到 x fulfilled 或 rejected;如果 x 是完成态,用相同的值完成 Promise ;如果 x 是拒绝态(rejected),用相同的原因拒绝 Promise。
因此上面的变体代码等效于下面的代码:
code snippetCopyjavascriptnew 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。
hard_invoke.js
注意适度用脑 😆。
code snippetCopyjavascriptasync function async1() {
console.log('async1 start')
new Promise((resolve, reject) => {
try {
throw new Error('error1')
} catch (e) {
console.log(e)
}
setTimeout(() => {
resolve('promise4')
}, 3 * 1000)
})
.then(
(res) => {
console.log(res)
},
(err) => {
console.log(err)
},
)
.finally((res) => {
console.log(res)
})
console.log(await async2())
console.log('async1 end')
}
function async2() {
console.log('async2')
return new Promise((resolve) => {
setTimeout(() => {
resolve(2)
}, 1 * 3000)
})
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
})
.then(() => {
console.log('promise2')
return new Promise((resolve) => {
resolve()
}).then(() => {
console.log('then 1-1')
})
})
.then(() => {
console.log('promise3')
})
console.log('script end')