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

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

正文



一、背景


缘起自一篇文章: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

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

00000.webp.jpg

Chrome 92

0000000.webp.jpg

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


目录
相关文章
|
13天前
|
前端开发 JavaScript 开发者
Async 和 Await 是基于 Promise 实现
【10月更文挑战第30天】Async和Await是基于Promise实现的语法糖,它们通过简洁的语法形式,借助Promise的异步处理机制,为JavaScript开发者提供了一种更优雅、更易于理解和维护的异步编程方式。
22 1
|
13天前
|
JSON 前端开发 JavaScript
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
21 1
|
1月前
|
前端开发 JavaScript
setTimeout、Promise、Async/Await 的区别
`setTimeout` 是用于延迟执行函数的简单方法;`Promise` 表示异步操作的最终完成或失败;`Async/Await` 是基于 Promise 的语法糖,使异步代码更易读和维护。三者都用于处理异步操作,但使用场景和语法有所不同。
|
1月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:深入了解 Promise 和 async/await
【10月更文挑战第8天】JavaScript 中的异步编程:深入了解 Promise 和 async/await
|
1月前
|
前端开发 JavaScript UED
深入了解JavaScript异步编程:回调、Promise与async/await
【10月更文挑战第11天】深入了解JavaScript异步编程:回调、Promise与async/await
13 0
|
2月前
|
前端开发 JavaScript
解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!
该文章教授了如何使用Promise和async/await来解决异步编程问题,从而避免回调地狱,使代码更加清晰和易于管理。
解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!
|
4月前
|
前端开发 JavaScript
Vue 中 Promise 的then方法异步使用及async/await 异步使用总结
Vue 中 Promise 的then方法异步使用及async/await 异步使用总结
140 1
|
3月前
|
前端开发 JavaScript 开发者
探索前端开发中的异步编程:Promise与Async/Await
在现代前端开发中,处理异步操作是至关重要的。本文将深入探讨异步编程的核心概念,重点比较JavaScript中的Promise与Async/Await两种异步编程方式。通过实例和比较,读者将能够理解这两种方法的优缺点,如何在实际开发中选择适合的异步编程模式,从而编写更简洁、可维护的代码。
|
4月前
|
前端开发 JavaScript
JavaScript异步编程:Promise与async/await的深入探索
【7月更文挑战第9天】Promise和async/await是JavaScript中处理异步编程的两大利器。Promise为异步操作提供了统一的接口和链式调用的能力,而async/await则在此基础上进一步简化了异步代码的书写和阅读。掌握它们,将使我们能够更加高效地编写出清晰、健壮的异步JavaScript代码。
|
4月前
|
前端开发 小程序 API
【微信小程序】使用 Promise、async 和 await 将异步API 改写为同步
【微信小程序】使用 Promise、async 和 await 将异步API 改写为同步
68 0