0
点赞
收藏
分享

微信扫一扫

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

吃面多放酱 2021-09-25 阅读 86

一、背景

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

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

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

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

二、找原因

本着寻根问底的初心,去找答案。其实去阅读 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

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

对比发现,不同浏览器下运行结果竟然不一样,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 标准导致的。

往下之前,明确两点:

上面的示例中 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')
    })
}

四、结论

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

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

五、References

举报

相关推荐

0 条评论