前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊一聊:一道 Promise 链式调用的题目

聊一聊:一道 Promise 链式调用的题目

作者头像
Chor
发布2020-07-27 17:36:21
5180
发布2020-07-27 17:36:21
举报
文章被收录于专栏:前端之旅
问题

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

代码语言:javascript
复制
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')
})

// 输出结果是什么?

第一眼看到的时候,你觉得输出结果是什么呢?可以先花几分钟仔细想一想

公布答案:

代码语言:javascript
复制
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
外部第三个then
外部第四个then
内部第二个then

不知道你有没有猜对?反正我猜错了。一开始我还以为是常规的 EventLoop 题目,无非就是考链式调用。但事实证明,它没有看上去那么简单。当时心里想的是,好奇怪,怎么和预想的不一样呢?

吃个午饭回来,本想继续看评论里有没有大神指点迷津或者是一起讨论下这道题,没想到的是,大神没出现,倒是出现了不少冷嘲热讽的人,大意是“写这样的代码就是菜,没有意义,不要浪费别人的时间”。又过了几分钟,发现楼主已经把帖子给删了。

…… 一时之间不知道说什么好,等到文章结束再来聊聊吧,我们还是先回到问题上。尽管这样的代码可能只是“为了面试而生”的,但我还是想弄清楚是怎么一回事,为何结果与猜想的不一样,于是这几天一直在翻阅网上的资料,请教网友们。到了今天,算是有点眉目了,所以在这里记录一下具体的分析过程。

注意:

  • 问题的解答来源于网上的相关文章和回答,我只是在此基础上整理分析思路和过程
  • 文章不会讨论 Promise/A+ 实现,ECMAScript 规范解读,webkit 源码等内容,但底下会有相关链接,想继续深挖的朋友可以看看
先从简单的开始分析

在讨论这段代码之前,我们先从一段相对简单的代码开始分析:

代码语言:javascript
复制
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

综上,执行顺序为:

代码语言:javascript
复制
promise1  
外部第一个then  
promise2   
内部第一个then  
外部第二个then   
内部第二个then
再看题目

那么,按照这个思路分析的话,文章开头那段代码的输出结果是什么呢?由于思路差不多,这里就直接写结果了:

代码语言:javascript
复制
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
外部第三个then
外部第四个then

当然,这个结果是错误的,下面才是正确的结果:

代码语言:javascript
复制
外部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 之后,会生成一个微任务并放入队列中,这个微任务可以近似理解为如下代码:
代码语言:javascript
复制
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 的回调
  6. 执行内部第一个 then 的回调,输出 内部第一个then,接着执行 retrun Promise.resolve(),按照前面说的,这会往队列中放入一个新生成的微任务 此时的队列: 外部第二个 then 的回调 → microTask
  7. 记住,内部第一个then的回调虽然执行完毕了,但是 then 返回的 promise 还没有 resolve,所以,内部第二个 then 的回调还不会进入队列。接着执行外部第二个 then 的回调,输出 外部第二个then,同时,外部第三个 then 的回调进入队列 此时的队列:microTask → 外部第三个 then 的回调 微任务执行完毕,第二轮事件循环结束。
  8. 执行 microTask,这将执行此前内部第一个 then 的回调返回的 promsie_0 的 then 方法,那么 then 的回调是否会马上进入队列呢?会的,因为 promsie_0 已经处于 resolved 状态 此时的队列:外部第三个 then 的回调 → promsie_0 的 then 的回调
  9. 执行外部第三个 then 的回调,输出 外部第三个then,同时,外部第四个 then 的回调进入队列 此时的队列:promsie_0 的 then 的回调 → 外部第四个 then 的回调
  10. 执行 promsie_0 的 then 的回调,这将会 resolve 内部第一个 then 返回的 promise_1。由于这个 thenresolve 了,所以后面跟着的内部第二个 then 的回调得以进入队列 此时的队列: 外部第四个 then 的回调 → 内部第二个 then 的回调
  11. 执行外部第四个 then 的回调,输出 外部第四个then。同时,外部第四个 then 返回的 promise 被 resolve,不过它后面没有跟着额外的 then,所以不再往队列中增加新的回调 此时的队列:内部第二个 then 的回调
  12. 执行内部第二个 then 的回调,输出 内部第二个then。同时,这个 then 返回的 promise 被 resolve,不过它后面没有跟着额外的 then,所以不再往队列中增加新的回调 此时的队列:空

整段代码的事件循环其实只有一轮,宏任务的执行负责分发微任务到队列中,而微任务在执行的时候又会产生其它微任务,后面其实一直都是在处理微任务了,直到清空队列,没有额外的微任务或者宏任务需要执行了,整段代码也就结束了。

综上,最终的输出是:

代码语言:javascript
复制
外部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”。我知道,也许真有人会这么想,但他们不会说出来,这对我来说是最大的善意了。国内技术社区缺乏的,往往并不是技术,而是一颗包容心以及足够友善的氛围。自己技术提高了,看一些问题自然会觉得很简单,但说实话,这不是你挖苦别人的资本,大家都是一步一个脚印慢慢走过来的。

参考链接:

关于promise输出顺序的疑问

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题
  • 先从简单的开始分析
  • 再看题目
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档