Promise 链式调用踩坑记录

微信扫一扫,分享到朋友圈

Promise 链式调用踩坑记录

问题

这是最近几天在掘金沸点看到的一道题目:

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 的回调全部放入队列

明确这几点之后,我们再来逐步分析这段代码:

  1. 执行宏任务,实例化 Promise,打印 promise1 ,之后调用了 resolve ,该 promise 被 resolve
  2. 外部第一个 then 执行,对应的回调马上进队列
  3. 外部第二个 then 执行,但是由于外部第一个 then 的回调还没执行,所以它返回的 promise 还没 resolve ,所以外部第二个 then 的回调暂时放着,不进队列
  4. 执行微任务,即外部第一个 then 的回调,打印 外部第一个 then
  5. 实例化第二个 Promsie,打印 promise2 ,之后调用了 resolve ,该 promise 被 resolve
  6. 内部第一个 then 执行,对应的回调马上进队列
  7. 内部第二个 then 执行,但是由于内部第一个 then 的回调还没执行,所以内部第一个 then 返回的 promsie 还没 resolve ,导致内部第二个 then 执行的回调暂时放着,不进队列
  8. 到这里,外部第一个 then 的回调其实已经执行完毕,所以外部第一个 then 返回的 promsie 被 resolve 了,一旦被 resolve ,和它挂钩的 then 的回调全部放入队列,所以外部第二个 then 的回调进队列
  9. 执行宏任务,无宏任务
  10. 执行微任务,队头是内部第一个 then ,于是打印 内部第一个 then ,由于内部第一个 then 的回调执行完毕,所以它返回的 promise 被 resolve 了,使得内部第二个 then 的回调进入队列
  11. 接着继续按队列执行,打印 外部第二个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 的回调才终于有机会进入队列。

在清楚这一点之后,我们再从头到尾分析一下这段代码:

第一轮事件循环

  1. 整体代码作为宏任务执行:实例化 promise,输出 外部promise ,之后调用 resolve ,promise 到达 resolved 状态

  2. 执行外部第一个 then ,由于 then 前面的 promsie 已经被 resolve ,所以 then 的回调进入队列。后面虽然相继执行了外部第二个、第三个、第四个 then ,但由于每个 then 前面的 promise 都还没有 resolve ,所以他们的回调都不会进入队列。

    此时的队列:外部第一个 then 的回调

  3. 宏任务执行完毕,查看微任务并执行:队列取出外部第一个 then 的回调执行,输出 外部第一个then ,接着实例化 promise,输出 内部promise ,之后调用 resolve ,该 promise 达到 resolved 状态

    此时的队列:空

  4. 执行内部第一个 then ,由于 then 前面的 promsie 已经被 resolve ,所以 then 的回调进入队列;执行内部第二个 then ,由于内部第一个 then 尚未 resolve ,所以它的回调暂时不进入队列

    此时的队列: 内部第一个 then 的回调

  5. 到这里,外部第一个 then 的回调执行完毕,并且返回一个非 thenable (返回 undefined ),所以这个 then 返回的 promise 被 resolve ,使得外部第二个 then 的回调进入队列。

    微任务执行完毕,第一轮事件循环结束。

    此时的队列:内部第一个 then 的回调 → 外部第二个 then 的回调

第二轮事件循环

  1. 查看宏任务,无宏任务,于是取队列的微任务执行

  2. 执行内部第一个 then 的回调,输出 内部第一个then ,接着执行 retrun Promise.resolve() ,按照前面说的,这会往队列中放入一个新生成的微任务

    此时的队列: 外部第二个 then 的回调 → microTask

  3. 记住,内部第一个then的回调虽然执行完毕了,但是 then 返回的 promise 还没有 resolve ,所以,内部第二个 then 的回调还不会进入队列。接着执行外部第二个 then 的回调,输出 外部第二个then ,同时,外部第三个 then 的回调进入队列

    此时的队列:microTask → 外部第三个 then 的回调

    微任务执行完毕,第二轮事件循环结束。

第三轮事件循环

  1. 查看宏任务,无宏任务,于是取队列的微任务执行

  2. 执行 microTask,这将执行此前内部第一个 then 的回调返回的 promsie_0 的 then 方法,那么 then 的回调是否会马上进入队列呢?会的,因为 promsie_0 已经处于 resolved 状态

    此时的队列:外部第三个 then 的回调 → promsie_0 的 then 的回调

  3. 执行外部第三个 then 的回调,输出 外部第三个then ,同时,外部第四个 then 的回调进入队列

    此时的队列:promsie_0 的 then 的回调 → 外部第四个 then 的回调

    微任务执行完毕,第二轮事件循环结束。

第四轮事件循环

  1. 查看宏任务,无宏任务,于是取队列的微任务执行

  2. 执行 promsie_0 的 then 的回调,这将会 resolve 内部第一个 then 返回的 promise_1。由于这个 thenresolve 了,所以后面跟着的内部第二个 then 的回调得以进入队列

    此时的队列: 外部第四个 then 的回调 → 内部第二个 then 的回调

  3. 执行外部第四个 then 的回调,输出 外部第四个then 。同时,外部第四个 then 返回的 promise 被 resolve ,不过它后面没有跟着额外的 then ,所以不再往队列中增加新的回调

    此时的队列:内部第二个 then 的回调

    微任务执行完毕,第二轮事件循环结束。

第五轮事件循环

  1. 查看宏任务,无宏任务,于是取队列的微任务执行

  2. 执行内部第二个 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,所以这篇文章就没办法往深的地方写了,也不会涉及原理,但如果你想从事件循环的角度分析这段代码,应该还是能提供一点帮助的。各位如果想继续深入挖掘的话,可以阅读文末链接的几篇文章。

最后想谈谈楼主删帖这件事情。不管出于什么原因,删帖都不算是好的解决方式。何况我能从他的描述看出,他自己是经过思考的,所问的也并不是几分钟就能讲清楚的问题。那么在这种情况下,和大家一起探讨这道题,对一个技术社区来说,不是再正常不过的事情了吗?什么时候开始,正常的提问还能够被一些毫无素质的人恶语相向?有问题的不是楼主,而是那些满怀恶意之人。我知道国内社区一直都有这种人存在,很久以前自己也遇到过这种事,我当然不会和他们多费口舌,但那时候的事情仍然给我带来了很糟心的体验。

国内技术社区缺乏的,往往并不是技术,而是一颗包容心以及足够友善的氛围。自己技术提高了,看一些问题会觉得很简单,但说实话,没必要刻意表现这种优越感,大家都是一步步慢慢走过来的。

参考链接:

关于promise输出顺序的疑问

深度揭秘 Promise 微任务注册和执行过程

Promise 链式调用顺序引发的思考

微信扫一扫,分享到朋友圈

Promise 链式调用踩坑记录

垃圾回收算法实现之 - 分代回收(完整可运行C语言代码)

上一篇

吉林现26800亩外来入侵物种黄花刺茄:植株果实均有毒

下一篇

你也可能喜欢

Promise 链式调用踩坑记录

长按储存图像,分享给朋友