关于 Await、Promise 执行顺序差异问题

配图源自 Freepik

一、背景

缘起自一篇文章:8 张图帮你一步步看清 async/await 和 promise 的执行顺序,文中所抛出的话题,本质上就是考察是否完全掌握了 JavaScript 的事件循环机制(Event Loop)罢了。

插个话,不同宿主环境(比如浏览器、Node),JS 的事件循环会稍有不同,本文则是基于浏览器环境下。至于其中差异并非本文讨论的内容,因此不展开讲述。

前面文章末尾或评论区提到的,同样一段代码在不同浏览器、或同一浏览器的不同版本,执行顺序存在差异。(代码就不贴上来了,可以点开链接去查看)

本人亲测结果,在 Chrome 92Safari 14.1.2 执行顺序仍有差异(2021.08)。

这种差异会带来什么影响呢?

实际应用场景几乎没有影响。所以不用担心,如果有人在项目中写出这样的代码,你可以去打死他了。请不要过分依赖异步操作的顺序。

一般来说,若再遇到 JavaScript 运行方面的差异,应以最新 Chrome 浏览器的行为为准。(跟 Chrome 浏览器的 V8 引擎更新策略有关)

二、找原因

本着寻根问底的初心,去找答案。其实去阅读 ECMAScript 标准是最直接、最权威的(例如,关于 Await 的标准在这里)。但由于功力不够,没办法完全看懂。

于是搜了好久,终于找到了一个相关的问题:async/await 在 Chrome 环境和 Node 环境的执行结果不一致,求解?以及贺老的回答

该问题中的示例(略微修改)如下:

async function foo() {
  console.log('a')
  await bar()
  console.log('b')
}

async function bar() {
  console.log('c')
}

foo()

new Promise(resolve => {
  console.log('d')
  resolve()
}).then(() => {
  console.log('e')
})

相信很多同学一下就写出了“正确”的打印顺序:a、c、d、b、e

我们执行代码并打印出来看下:

Chrome 92
Safari 14.1.2

对比发现,不同浏览器下运行结果竟然不一样,Why?

  • 最新版 Chrome 浏览器打印结果为:a、c、d、b、e
  • 最新版 Safari 浏览器打印结果为:a、c、d、e、b
  • 在 Node 14.16.0 环境下,运行结果同 Chrome 浏览器。

造成以上差异的根本原因是,ECMAScript 就 Await 标准有所调整,最新规定是 await 将直接使用 Promise.resolve() 相同的语义。正是因为此次调整,导致了不同 JS 引擎或者同一 JS 引擎的不同版本,在解析同一脚本会出现结果的差异。

上面示例中 await bar() 的计算结果(指 bar() 返回值)就是一个 Promise 对象。根据 Promise.resolve() 的语法,若参数是一个 Promise 实例对象,将会不做任何修改、原封不动地返回该实例。

const p1 = new Promise(resolve => resolve(1))
const p2 = Promise.resolve(p1)
console.log(p1 === p2) // true
// ⚠️ 注意,关于 Promise.resolve() 在 Chrome 与 Safari 表现是一致的。

其实无需过分担心这种差异,对平时写项目有什么影响,如果在真正项目写出类似的逻辑,确实该反思一下。但是......面试官可能会问哦,前面文章提到的那道题好像就是头条的面试题。

三、原因剖析

这种差异,是 JavaScript 引擎在实现时没有严格遵循 ECMAScript 标准导致的。

往下之前,明确两点:

  • Promise 对象的构造方法内属于同步任务,而 Promise.prototype.then() 才属于异步任务(微任务,它的执行顺序后于同步任务)

  • Promise.resolve() 方法,若参数为 Promise 对象,将会直接返回该对象,而不是返回一个全新的 Promise 对象。

  • 只有当 Promise 对象的状态发生变化,才会被放入微任务队列。

上面的示例中 acd 的顺序都没有争议,因此我们简化一下示例:

// 其中 p1、p2 都是状态为 fulfilled 的 Promise 对象
async function foo() {
  await p1
  console.log('b')
}

foo()

p2.then(() => {
  console.log('e')
})

关键点在于 await p1 的语义是什么?一般而言,我们可以把:

async function foo() {
  await p1
  console.log('b')
}

理解为:

function foo() {
  return RESOLVE(p1).then(() => {
    console.log('b')
  })
}

按目前的标准定义 RESOLVE(p1) 等同于 Promise.resolve(p1),因此 RESOLVE(p1) 结果就是 p1。根据代码逻辑可知 p1p2 更早地放入微任务队列。本着先进先出的原则,会先执行微任务 p1,后执行微任务 p2,因此先后打印出 be

但是旧版的 JS 引擎在实现 RESOLVE(p1) 的问题上,与当前标准有微妙而重要的区别。区别在于,即使 p1 是一个 Promise 对象,RESOLVE(p1) 仍会返回一个全新Promise 对象(假设为 p3)。

换句话说,就是执行 p1.then() 时,又产生了一个微任务 p3,并放入微任务队列。还是本着先进先出的原则,接着执行微任务 p2 并打印 e。等 p2 执行完毕,接着执行微任务 p3,然后打印出 b。因此先后顺序是 eb

function foo() {
  return RESOLVE(p1).then(() => {
    console.log('b')
  })
}

// 相当于
function foo() {
  return new Promise(resolve => resolve(p1)) // 相当于微任务 p1
    .then(() => { // 相当于微任务 p3
      console.log('b')
    })
}

虽然我认为自己懂 Async 内部执行器的执行过程,但是我自认为对本案例解释得不够好。就是那种“懂但不知道怎么表达出来”的感觉。如果看懵了的话,建议直接看贺老的回答

四、结论

综上,不同浏览器下执行顺序不一样,应该就是 JS 引擎(其中 Chrome、Node 是 V8 引擎,而 Safari 是 JavaScriptCore 引擎。)底层实现 await 语法的方式略有不同。若严格遵循 ECMAScript 标准的话, 执行结果与最新的 Chrome 浏览器应该是一致的。

前面提到若有差异,一般以最新版本的 Chrome 为准,原因是:Chrome 浏览器每次升级都会同时更新到 V8 的最新版。而 Node 更新小版本时,V8 也只更新小版本,只有 Node 更新大版本时才会更新 V8 大版本。所以,绝大部分时候 Node 的 V8 会比同时期的 Chrome 的 V8 要落后。

五、References

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

推荐阅读更多精彩内容