这是最近几天在掘金沸点看到的一道题目:
new Promise((resolve,reject) => {
console.log('外部promise')
resolve()
})
.then(() => {
console.log('外部第一个then')
new Promise((resolve,reject) => {
console.log('内部promise')
resolve()
})
.then(() => {
console.log('内部第一个then')
return Promise.resolve()
})
.then(() => {
console.log('内部第二个then')
})
})
.then(() => {
console.log('外部第二个then')
})
.then(() => {
console.log('外部第三个then')
})
.then(() => {
console.log('外部第四个then')
})
// 输出结果是什么?
第一眼看到的时候,你觉得输出结果是什么呢?可以先花几分钟仔细想一想
公布答案:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then
不知道你有没有猜对?反正我猜错了。一开始我还以为是常规的 EventLoop 题目,无非就是考链式调用。但事实证明,它没有看上去那么简单。当时心里想的是,好奇怪,怎么和预想的不一样呢?
吃个午饭回来,本想继续看评论里有没有大神指点迷津或者是一起讨论下这道题,没想到的是,大神没出现,倒是出现了不少冷嘲热讽的人,大意是“写这样的代码就是菜,没有意义,不要浪费别人的时间”。又过了几分钟,发现楼主已经把帖子给删了。
…… 一时之间不知道说什么好,等到文章结束再来聊聊吧,我们还是先回到问题上。尽管这样的代码可能只是“为了面试而生”的,但我还是想弄清楚是怎么一回事,为何结果与猜想的不一样,于是这几天一直在翻阅网上的资料,请教网友们。到了今天,算是有点眉目了,所以在这里记录一下具体的分析过程。
注意:
在讨论这段代码之前,我们先从一段相对简单的代码开始分析:
new Promise((resolve,reject)=>{
console.log("promise1")
resolve( )
})
.then(()=>{
console.log("外部第一个then")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("内部第一个then")
}).then(()=>{
console.log("内部第二个then")
})
})
.then(()=>{
console.log("外部第二个then")
})
先说几个基本的结论:
then
的回调到底什么时候进入队列?
调用 then
,里面的回调不一定会马上进入队列
then
前面的 promise 已经被 resolve
,那么调用 then
后,回调就会进入队列then
前面的 promise 还没有被 resolve
,那么调用 then
后,回调不会进入队列,而是先暂时存着,等待 promsie 被 resolve
之后再进队列。then
前面的 promise 怎么才算被 resolve
呢?
resolve()
后它就被 resolve
了then
返回的,那么 then
的回调执行完毕之后它就被 resolve
了。resolve
之后会做什么?
then
的回调全部放入队列明确这几点之后,我们再来逐步分析这段代码:
promise1
,之后调用了 resolve
,该 promise 被 resolve
then
执行,对应的回调马上进队列then
执行,但是由于外部第一个 then
的回调还没执行,所以它返回的 promise 还没 resolve
,所以外部第二个 then
的回调暂时放着,不进队列then
的回调,打印 外部第一个 then
promise2
,之后调用了 resolve
,该 promise 被 resolve
then
执行,对应的回调马上进队列then
执行,但是由于内部第一个 then
的回调还没执行,所以内部第一个 then
返回的 promsie 还没 resolve
,导致内部第二个 then
执行的回调暂时放着,不进队列then
的回调其实已经执行完毕,所以外部第一个 then
返回的 promsie 被 resolve
了,一旦被 resolve
,和它挂钩的 then
的回调全部放入队列,所以外部第二个 then
的回调进队列then
,于是打印 内部第一个 then
,由于内部第一个 then
的回调执行完毕,所以它返回的 promise 被 resolve
了,使得内部第二个 then
的回调进入队列外部第二个then
,使得这个 then
返回的 promise 被 resolve
,不过它没有后续的 then
,所以不管它接着继续按队列执行,打印最后的 内部第二个then
综上,执行顺序为:
promise1
外部第一个then
promise2
内部第一个then
外部第二个then
内部第二个then
那么,按照这个思路分析的话,文章开头那段代码的输出结果是什么呢?由于思路差不多,这里就直接写结果了:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
外部第三个then
外部第四个then
当然,这个结果是错误的,下面才是正确的结果:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then
在一开始分析的时候,我忽略了 return Promise.resolve()
这个语句,以为它就只是同步返回一个 Promise 实例而已,但实际上, then
的回调的返回值是需要引起关注的。
前面说过,如果 promise 是 then
返回的,那么 then
的回调执行完毕之后它就被 resolve
了,这里其实要细分情况:
then
的回调返回的不是一个 thenable
(具有 then
方法的 object
),那么,这个返回值将被 then
返回的 promise 用来进行 resolve
。而这个 promise 一旦被 resolve
,则后面调用 then
的时候,then
的回调可以马上进入队列(严格地说,进入队列的不是回调,而是用于调用回调的某个微任务)。then
的回调返回的是一个 thenable
,比如说返回一个 promise_0,那么,这个 promise_0 会直接决定 then
返回的 promise_1 的状态(pending,resolve,reject)。而且,即使 promise_0 本身已经被 resolve
了,promise_1 也不会马上被 resolve
,具体地说,需要经历下面的过程:
在返回 promise_0 之后,会生成一个微任务并放入队列中,这个微任务可以近似理解为如下代码:microTask() => {
promise_0.then(() => {
promise_1.resolve()
})
}
它所做的事情,就是调用 promise_0 的 then
方法,从而将 then
的回调放入队列中,而直到回调被执行的时候,promise_1 才终于被 resolve
或者 reject
,它后面的 then
的回调才终于有机会进入队列。
在清楚这一点之后,我们再从头到尾分析一下这段代码:
外部promise
,之后调用 resolve
,promise 到达 resolved
状态then
,由于 then
前面的 promsie 已经被 resolve
,所以 then
的回调进入队列。后面虽然相继执行了外部第二个、第三个、第四个 then
,但由于每个 then
前面的 promise 都还没有 resolve
,所以他们的回调都不会进入队列。
此时的队列:外部第一个 then
的回调then
的回调执行,输出 外部第一个then
,接着实例化 promise,输出 内部promise
,之后调用 resolve
,该 promise 达到 resolved
状态
此时的队列:空then
,由于 then
前面的 promsie 已经被 resolve
,所以 then
的回调进入队列;执行内部第二个 then
,由于内部第一个 then
尚未 resolve
,所以它的回调暂时不进入队列
此时的队列: 内部第一个 then
的回调then
的回调执行完毕,并且返回一个非 thenable
(返回undefined
),所以这个 then
返回的 promise 被 resolve
,使得外部第二个 then
的回调进入队列。
此时的队列:内部第一个 then
的回调 → 外部第二个 then
的回调then
的回调,输出 内部第一个then
,接着执行 retrun Promise.resolve()
,按照前面说的,这会往队列中放入一个新生成的微任务
此时的队列: 外部第二个 then
的回调 → microTaskthen
返回的 promise 还没有 resolve
,所以,内部第二个 then
的回调还不会进入队列。接着执行外部第二个 then
的回调,输出 外部第二个then
,同时,外部第三个 then
的回调进入队列
此时的队列:microTask → 外部第三个 then
的回调
微任务执行完毕,第二轮事件循环结束。then
的回调返回的 promsie_0 的 then
方法,那么 then
的回调是否会马上进入队列呢?会的,因为 promsie_0 已经处于 resolved
状态
此时的队列:外部第三个 then
的回调 → promsie_0 的 then
的回调then
的回调,输出 外部第三个then
,同时,外部第四个 then
的回调进入队列
此时的队列:promsie_0 的 then
的回调 → 外部第四个 then
的回调then
的回调,这将会 resolve
内部第一个 then
返回的 promise_1。由于这个 then
被 resolve
了,所以后面跟着的内部第二个 then
的回调得以进入队列
此时的队列: 外部第四个 then
的回调 → 内部第二个 then
的回调then
的回调,输出 外部第四个then
。同时,外部第四个 then
返回的 promise 被 resolve
,不过它后面没有跟着额外的 then
,所以不再往队列中增加新的回调
此时的队列:内部第二个 then
的回调then
的回调,输出 内部第二个then
。同时,这个 then
返回的 promise 被 resolve
,不过它后面没有跟着额外的 then
,所以不再往队列中增加新的回调
此时的队列:空整段代码的事件循环其实只有一轮,宏任务的执行负责分发微任务到队列中,而微任务在执行的时候又会产生其它微任务,后面其实一直都是在处理微任务了,直到清空队列,没有额外的微任务或者宏任务需要执行了,整段代码也就结束了。
综上,最终的输出是:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then
与实际的输出结果完全一致。
这样分析就结束了。其实核心就在于判断 then
的回调进入队列的时机,而它入队的时机又取决于前面 promise_1 被 resolve
的时机。一开始认为在同步执行 return Promise.resolve()
(记作 promise_0)的时候,前面 then
的回调就执行完毕了, promise_1 就已经被 resolve
了。但实际上,如果回调返回的是一个 thenable
,则属于特殊情况,它会导致生成一个新的微任务放到队列中, promise_1 也因此不会马上被 resolve
,而是等到 promise_0 的 then
的回调被执行的时候,才会被 resolve
。
分析思路基本是参考思否的 @fefe 大佬的,他在回答中提到了规范的一些内容,不过我没有了解过 Promise 的内部实现,也没有研读过 spec,所以这篇文章就没办法往深的地方写了,也不会涉及原理,但如果你想从事件循环的角度分析这段代码,应该还是能提供一点帮助的。各位如果想继续深入挖掘的话,可以阅读文末链接的几篇文章。
最后想谈谈楼主删帖这件事情。我觉得在技术社区提问之前,如果能确保:
而在提问的时候,能确保:
那么这个提问毫无疑问就已经是合格的了,甚至说已经超出了一般提问的水平(因为上面说的几点,其实有很多人是做不到的)。但我看到的却是,这样的一个提问受到了一些人的冷嘲热讽,这种现象发生在一个技术社区,并不正常。
不瞒各位,我偶尔也会在 StackOverflow 上问一些比较小白的问题,但从不会有人吐槽说 “You are foolish”。我知道,也许真有人会这么想,但他们不会说出来,这对我来说是最大的善意了。国内技术社区缺乏的,往往并不是技术,而是一颗包容心以及足够友善的氛围。自己技术提高了,看一些问题自然会觉得很简单,但说实话,这不是你挖苦别人的资本,大家都是一步一个脚印慢慢走过来的。
参考链接: