本文深入解析 async/await 导致的性能瓶颈问题,即“async/await 地狱”。通过对比串行与并发执行模式,指出不必要的等待会拖慢程序。核心解决方案包括识别依赖关系、封装独立异步任务,并利用 Promise.all 并行处理无关联的异步请求,从而优化执行效率。
前言
async/await 的基本概念
async/await 的基本概念
async/await 本质上是 co 模块和生成器函数的语法糖,旨在通过更清晰的语义简化 JavaScript 异步代码的编写。
了解 co 模块的开发者知道,该模块由 TJ 大神开发,用于通过生成器函数处理异步流程,可视为生成器函数的执行器。async/await 是对 co 模块的升级,内置了生成器函数的执行逻辑,不再依赖外部 co 模块。此外,async 函数始终返回 Promise 对象。
无论是 co 模块还是 async/await,均以 Promise 为基础单元。对 Promise 不熟悉的同学建议先深入理解 Promise 机制。
虽然 async/await 写法简洁,但使用时需注意潜在问题。
async/await 虽然解决了传统的回调地狱问题,却可能引入新的“async/await 地狱”。

什么是 async/await 地狱
什么是 async/await 地狱
在 JavaScript 异步编程中,开发者常使用大量 await 语句。若后续语句并不依赖前置结果,这种串行等待会导致不必要的性能损耗。
async/await 地狱示例
以下代码演示了一个购买披萨和饮料的流程:
(async () => {
const pizzaData = await getPizzaData() // async call
const drinkData = await getDrinkData() // async call
const chosenPizza = choosePizza() // sync call
const chosenDrink = chooseDrink() // sync call
await addPizzaToCart(chosenPizza) // async call
await addDrinkToCart(chosenDrink) // async call
orderItems() // async call
})()
该代码运行无误,但并非最佳实现,因为它引入了不必要的等待时间。
说明
代码封装在异步 IIFE(立即调用函数表达式)中,按以下顺序执行:
获取披萨列表 获取饮料列表 从列表中选定披萨 从列表中选定饮料 将选定的披萨加入购物车 将选定的饮料加入购物车 提交购物车订单
问题分析
此处存在逻辑缺陷:为何选择披萨需等待获取饮料列表?这两个操作无关联。实际关联操作分为两组:
获取披萨列表 → 选择披萨 → 加入购物车
获取饮料列表 → 选择饮料 → 加入购物车
这两组操作应并发执行,而非串行等待。
另一个更典型的反面案例
此代码片段尝试并发发送购物车商品的订购请求。
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // async call
}
}
当前实现中,for 循环必须等待 sendRequest() 完成才能进入下一次迭代。实际上无需逐个等待,应尽快发出所有请求,随后统一等待所有请求完成。
现在你对 async/await 地狱有了更深的理解,接下来探讨另一个关键问题。
如果忘记使用 await 关键字会怎样?
调用异步函数时若省略 await,意味着无需等待其执行结果。异步函数将直接返回一个 Promise 对象,供后续使用。
(async () => {
const value = doSomeAsyncTask()
console.log(value) // an unresolved promise
})()
若程序未明确等待函数执行完毕,可能直接退出导致异步任务未完成。因此,必须使用 await 关键字确保异步操作完成。
Promise 有一个特性:可在某行代码获取 Promise,并在后续任意位置等待其 resolve。这是解决 async/await 地狱的关键。
(async () => {
const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // the actual value
})()
如示例所示,doSomeAsyncTask 直接返回 Promise,且异步任务已立即开始执行。要获取返回值,需使用 await 通知程序等待结果。
如何避免 async/await 地狱
如何避免 async/await 地狱
首先,需明确哪些命名存在前后依赖关系。
其次,将具有依赖关系的系列操作分组,合并为独立的异步操作。
最后,并发执行这些异步函数。
以下代码重写了上述示例:
async function selectPizza() {
const pizzaData = await getPizzaData() // async call
const chosenPizza = choosePizza() // sync call
await addPizzaToCart(chosenPizza) // async call
}
async function selectDrink() {
const drinkData = await getDrinkData() // async call
const chosenDrink = chooseDrink() // sync call
await addDrinkToCart(chosenDrink) // async call
}
(async () => {
const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // async call
})()
// Although I prefer it this way
(async () => {
Promise.all([selectPizza(), selectDrink()].then(orderItems) // async call
})()
将语句拆分为两个函数。在各自函数内部,每个语句依赖前一个语句的执行结果。随后同时执行 selectPizza() 和 selectDrink() 两个函数。
在第二个示例中,需处理未知数量的 Promise。解决方法很简单:创建数组存储所有 Promise,并使用 Promise.all() 方法并行执行:
async function orderItems() {
const items = await getCartItems() // async call
const noOfItems = items.length
const promises = []
for(var i = 0; i < noOfItems; i++) {
const orderPromise = sendRequest(items[i]) // async call
promises.push(orderPromise) // sync call
}
await Promise.all(promises) // async call
}
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。