async/await 地狱的成因与避免方案详解

作者:纸飞机

时间:2026-10-22

浏览:478

本文深入解析 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 地狱

什么是 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
}

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章