问题
这是最近几天在掘金沸点看到的一道题目:
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 题目,无非就是考链式调用。但事实证明,它没有看上去那么简单。当时心里想的是,好奇怪,怎么和预想的不一样呢?
吃个午饭回来,本想继续看评论里有没有大神指点迷津或者是一起讨论下这道题,没想到的是,大神没出现,倒是出现了不少冷嘲热讽的人,大意是“写这样的代码就是菜,没有意义,不要浪费别人的时间”。又过了几分钟,发现楼主已经把帖子给删了。
… 一时之间不知道说什么好,等到文章结束再来聊聊吧,我们还是先回到问题上。尽管这样的代码可能只是“为了面试而生”的,但我还是想弄清楚是怎么一回事,为何结果与猜想的不一样,于是这几天一直在翻阅网上的资料,请教网友们。到了今天,算是有点眉目了,所以在这里记录一下具体的分析过程。
注意:
- 问题的解答来源于网上的相关文章和回答,我只是在此基础上整理分析思路和过程
- 文章不会讨论 Promise/A+ 实现,ECMAScript 规范解读,webkit 源码等内容,但底下会有相关链接,想继续深挖的朋友可以看看
先从简单的开始分析
在讨论这段代码之前,我们先从一段相对简单的代码开始分析:
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
呢?如果 promsie 是实例化形成的,那么调用
resolve()
后它就被resolve
了如果 promise 是
then
返回的,那么then
的回调执行完毕之后它就被resolve
了。promise 被
resolve
之后会做什么?- 会把此前和该 promise 挂钩的
then
的回调全部放入队列
- 会把此前和该 promise 挂钩的
明确这几点之后,我们再来逐步分析这段代码:
- 执行宏任务,实例化 Promise,打印
promise1
,之后调用了resolve
,该 promise 被resolve
- 外部第一个
then
执行,对应的回调马上进队列 - 外部第二个
then
执行,但是由于外部第一个then
的回调还没执行,所以它返回的 promise 还没resolve
,所以外部第二个then
的回调暂时放着,不进队列 - 执行微任务,即外部第一个
then
的回调,打印外部第一个 then
- 实例化第二个 Promsie,打印
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,输出
外部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
,接着执行return Promise.resolve()
,按照前面说的,这会往队列中放入一个新生成的微任务此时的队列: 外部第二个
then
的回调 → microTask记住,内部第一个then的回调虽然执行完毕了,但是
then
返回的 promise 还没有resolve
,所以,内部第二个then
的回调还不会进入队列。接着执行外部第二个then
的回调,输出外部第二个then
,同时,外部第三个then
的回调进入队列此时的队列:microTask → 外部第三个
then
的回调微任务执行完毕,第二轮事件循环结束。
执行 microTask,这将执行此前内部第一个
then
的回调返回的 promsie_0 的then
方法,那么then
的回调是否会马上进入队列呢?会的,因为 promsie_0 已经处于resolved
状态此时的队列:外部第三个
then
的回调 → promsie_0 的then
的回调执行外部第三个
then
的回调,输出外部第三个then
,同时,外部第四个then
的回调进入队列此时的队列:promsie_0 的
then
的回调 → 外部第四个then
的回调执行 promsie_0 的
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”。我知道,也许真有人会这么想,但他们不会说出来,这对我来说是最大的善意了。国内技术社区缺乏的,往往并不是技术,而是一颗包容心以及足够友善的氛围。自己技术提高了,看一些问题自然会觉得很简单,但说实话,这不是你挖苦别人的资本,大家都是一步一个脚印慢慢走过来的。
参考链接: