内容简介:这是最近几天在掘金沸点看到的一道题目:第一眼看到的时候,你觉得输出结果是什么呢?可以先花几分钟仔细想一想。
问题
这是最近几天在掘金沸点看到的一道题目:
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
了。
- 如果 promsie 是实例化形成的,那么调用
-
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
,接着执行retrun 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,所以这篇文章就没办法往深的地方写了,也不会涉及原理,但如果你想从事件循环的角度分析这段代码,应该还是能提供一点帮助的。各位如果想继续深入挖掘的话,可以阅读文末链接的几篇文章。
最后想谈谈楼主删帖这件事情。不管出于什么原因,删帖都不算是好的解决方式。何况我能从他的描述看出,他自己是经过思考的,所问的也并不是几分钟就能讲清楚的问题。那么在这种情况下,和大家一起探讨这道题,对一个技术社区来说,不是再正常不过的事情了吗?什么时候开始,正常的提问还能够被一些毫无素质的人恶语相向?有问题的不是楼主,而是那些满怀恶意之人。我知道国内社区一直都有这种人存在,很久以前自己也遇到过这种事,我当然不会和他们多费口舌,但那时候的事情仍然给我带来了很糟心的体验。
国内技术社区缺乏的,往往并不是技术,而是一颗包容心以及足够友善的氛围。自己技术提高了,看一些问题会觉得很简单,但说实话,没必要刻意表现这种优越感,大家都是一步步慢慢走过来的。
参考链接:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 如何低侵入的记录调用日志
- 微信小程序如何调用后台service的简单记录
- 直观讲解-RPC调用和HTTP调用的区别
- 调用链系列一:解读UAVStack中的调用链技术
- 调用链系列二:解读UAVStack中的调用链技术
- 调用链系列三:解读UAVStack中的调用链技术
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。