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

image

前言

近一个多月没有写博客了,前阵子一个朋友问我一个关于 Promise 链式调用执行顺序的问题

image

凭借我对 Promise 源码的了解,这种问题能难住我?

image

然后我理所当然的回答错了

image

之后再次翻阅了一遍曾经手写的 Promise,理清了其中的缘由,写下这篇文章,希望对 Promise 有更深一层的理解

问题

题目是这样的,为了更加语义化我将打印的字符串做了一些修改

new Promise((resolve, reject) => {
  console.log("log: 外部promise");
  resolve();
})
  .then(() => {
    console.log("log: 外部第一个then");
    new Promise((resolve, reject) => {
      console.log("log: 内部promise");
      resolve();
    })
      .then(() => {
        console.log("log: 内部第一个then");
      })
      .then(() => {
        console.log("log: 内部第二个then");
      });
  })
  .then(() => {
    console.log("log: 外部第二个then");
  });
  
// log: 外部promise
// log: 外部第一个then
// log: 内部promise
// log: 内部第一个then
// log: 外部第二个then
// log: 内部第二个then

它的考点并不仅限于 Promise 本身,同时还考察 Promise 链式调用之间的执行顺序,在开始解析之前,首先要清楚 Promise 能够链式调用的原理,即

promise 的 then/catch 方法执行后会也返回一个 promise

这里先抛出结论,然后再对题目进行解析

结论1

当执行 then 方法时,如果前面的 promise 已经是 resolved 状态,则直接将回调放入微任务队列中

执行 then 方法是同步的,而 then 中的回调是异步的

new Promise((resolve, reject) => {
  resolve();
}).then(() => {
  console.log("log: 外部第一个then");
});

实例化 Promise 传入的函数是同步执行的,then 方法也是同步执行的,但 then 中的回调会先放入微任务队列,等同步任务执行完毕后,再依次取出执行,换句话说只有回调是异步的

同时在同步执行 then 方法时,会进行判断:

  • 如果前面的 promise 已经是 resolved 状态,则会立即将回调推入微任务队列(但是执行回调还是要等到所有同步任务都结束后)
  • 如果前面的 promise 是 pending 状态则会将回调存储在 promise 的内部,一直等到 promise 被 resolve 才将回调推入微任务队列

结论2

当一个 promise 被 resolve 时,会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中

如何理解通过 then 给这个 promise 注册的所有回调,考虑以下案例

let p = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000);
});
p.then(() => {
  console.log("log: 外部第一个then");
});
p.then(() => {
  console.log("log: 外部第二个then");
});
p.then(() => {
  console.log("log: 外部第三个then");
});

1 秒后变量 p 才会被 resolve,但是在 resolve 前通过 then 方法给它注册了 3 个回调,此时这 3 个回调不会被执行,也不会被放入微任务队列中,它们会被 p 内部储存起来,等到 p 被 resolve 后,依次将这 3 个回调推入微任务队列,此时如果没有同步任务就会逐个取出再执行

另外还有几点需要注意:

  1. 对于普通的 promise 来说,当执行完 resolve 函数时,promise 状态就为 resolved

而 resolve 函数就是在实例化 Promise 时,传入函数的第一个参数

new Promise(resolve => {
  resolve();
});

它的作用除了将当前的 promise 由 pending 变为 resolved,还会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中,很多人以为是由 then 方法来触发它保存回调,而事实上是由 promise 的 resolve 来触发的,then 方法只负责注册回调

具体的行为可以参考底部链接

  1. 对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve

同步返回值的意思换句话说,如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后再 resolve(这句话有点像绕口令哈哈哈~)

new Promise((resolve, reject) => {
  resolve();
})
  .then(() =>
    new Promise((resolve, reject) => {
      resolve();
    }).then(() => {
      console.log("log: 内部第一个then");
    })
  )
  .then(() => {
    console.log("log: 外部第二个then");
  });
  
  // log: 内部第一个then
  // log: 外部第二个then

这里外部的第一个 then 的回调返回了一个 promise,所以外部第一个 then 返回的 promise 需要等到内部整个 promise (红框) 被 resolve 后才会被 resolve

image

当打印 log: 内部第一个then 后,回调执行完毕,蓝框的 promise 被 resolve,然后外部第一个 then 返回的 promise 才被 resolve

随后遍历之前通过 then 给外部第一个 then 返回的 promise 注册的所有回调(黄框),放入微任务队列,等同步任务执行完毕后,依次取出执行,最终打印 log: 外部第二个then

解析

分析完 promise 和 then 的行为后,我们结合代码来解析问题(建议分屏,比对问题章节中的案例代码查看解析)

首先 Promise 实例化时,同步执行函数,打印 log: 外部promise,然后执行 resolve 函数,将 promise 变为 resolved,但由于此时 then 方法还未执行,所以遍历所有 then 方法注册的回调时什么也不会发生(结论2第一条)

此时剩余任务如下:

主线程:外部第一个 then,外部第二个 then

微任务队列:空

接着执行外部第一个 then(以下简称:外1then),由于前面的 promise 已经被 resolve,所以立即将回调放入微任务队列(结论1)

主线程:外2then

微任务队列:外1then 的回调

但是由于此时这个回调还未执行,所以外1then 返回的 promise 仍为 pending 状态(结论2第二条),继续同步执行外2then,由于前面的 promise 是 pending 状态,所以外2then 的回调也不会被推入微任务队列也不会执行(结论2案例)

主线程:空

微任务队列:外1then 的回调

当主线程执行完毕后,执行微任务,也就是外1then 的回调,回调中首先打印log: 外部第一个then

随后实例化内部 promise,在实例化时执行函数,打印 log: 内部promise,然后执行 resolve 函数(结论1),接着执行到内部的第一个 then(内1then),由于前面的 promise 已被 resolve,所以将回调放入微任务队列中(结论1)

主线程:内2then

微任务队列:内1then 的回调

由于正在执行外1then 的回调,所以外1then 返回的 promise 仍是 pending 状态,外2then 的回调仍不会被注册也不会被执行

接着同步执行内2then,由于它前面的 promise (内1then 返回的 promise) 是 pending 状态(因为内1then 的回调在微任务队列中,还未执行),所以内2then 的回调和外2then 的回调一样,不注册不执行(结论2案例)

主线程:空

微任务队列:内1then 的回调

此时外1then 的回调全部执行完毕,外1then 返回的 promise 的状态由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们的回调放入微任务队列中(结论2),也就是外2then 的回调

主线程:空

微任务队列:内1then 的回调,外2then 的回调

此时主线程逻辑执行完毕,取出第一个微任务执行

主线程:内1then 的回调

微任务队列:外2then 的回调

执行内1then 的回调打印 log: 内部第一个then,回调执行完毕后,内1then 返回的 promise 由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们的回调放入微任务队列中(结论2),也就是内2then 的回调

主线程:空

微任务队列:外2then 的回调,内2then 的回调

执行外2then 的回调打印 log: 外部第二个then,回调执行完毕,外2then 返回的 promise 由 pending 变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们放入微任务队列中(结论2)

这时由于外2 then 返回的 promise 没有再进一步的链式调用了,主线程任务结束

主线程:空

微任务队列:内2then 的回调

接着取出微任务,执行内2then 的回调打印 log: 内部第二个then,内2then 返回的 promise 的状态变为 resolved(结论2第二条),同时遍历之前通过 then 给这个 promise 注册的所有回调(没有),至此全部结束

参考资料

我自己写的 promise~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,681评论 0 5
  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,673评论 1 56
  • JS的运行机制 先来一个今日头条的面试题 1. 单线程的JavaScript js是单线程的,基于事件循环,非阻塞...
    行动派巨人阅读 23,118评论 10 38
  • 火车上走廊里有一对母女,应该是妈妈带着女儿回姥姥家,她俩站在走廊里看着窗外的风景。 “妈妈,外面的房子好破啊。”小...
    柠檬和咖啡阅读 162评论 0 0
  • 【究竟想成为怎样的老师】天网计算机学校
    刘大帅54564阅读 225评论 0 0