关于 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

  • async await 和 promise 微任务执行顺序问题
  • async/await 在 Chrome 环境和 Node 环境的执行结果不一致,求解?
  • Faster async functions and promises

你可能感兴趣的:(关于 Await、Promise 执行顺序差异问题)