async/await 将我们从回调地狱中释放出来,但人们已经开始滥用它,导致异步/等待地狱的诞生。
在本文中,我将尝试解释async/await 地狱是什么,我还将分享一些提示以逃避它。
什么是异步/等待地狱
在使用异步JavaScript时,人们经常一个接一个地编写多个语句,并在函数调用之前等待等待。这会导致性能问题,因为很多时候一个语句不依赖于前一个语句 - 但您仍需要等待前一个语句完成。
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中。以下确切的顺序发生以下情况:
- 获取比萨饼列表。
- 获取饮料清单。
- 从列表中选择一个比萨饼。
- 从列表中选择一种饮料。
- 将选择的比萨添加到购物车。
- 将所选饮料添加到购物车中。
- 订购购物车中的商品。
那有什么不对?
正如我先前所强调的那样,所有这些陈述都是逐一执行的。这里没有并发性。仔细想想:为什么我们在尝试获取饮料清单之前等待获取比萨饼列表?我们应该尝试将两个列表放在一起。然而,当我们需要选择比萨饼时,我们需要事先获得比萨饼列表。饮料也一样。
因此,我们可以得出结论,与披萨相关的工作和饮料相关的工作可以并行发生,但是与披萨相关的工作中涉及的各个步骤需要按顺序(逐个)发生。
另一个糟糕的实施例子
此JavaScript代码段将获取购物车中的商品并发出订购请求。
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()
函数完成才能继续下一次迭代。但是,我们实际上并不需要等待。我们希望尽快发送所有请求,然后我们可以等待所有请求完成。
我希望你现在越来越接近理解什么是异步/等待地狱以及它对程序性能的影响有多严重。现在我想问你一个问题。
如果我们忘记了await关键字怎么办?
如果在调用异步函数时忘记使用await,则该函数开始执行。这意味着执行该功能不需要等待。异步函数将返回一个promise,您可以在以后使用它。
(async () => {
const value = doSomeAsyncTask()
console.log(value) // an unresolved promise
})()
另一个结果是编译器不会知道您要等待函数完全执行。因此,编译器将退出程序而不完成异步任务。所以我们确实需要await关键字。
承诺的一个有趣的特性是你可以在一行中得到一个承诺并等待它在另一行中解决。这是逃避异步/等待地狱的关键。
(async () => {
const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // the actual value
})()
如你所见,doSomeAsyncTask()
正在回复一个承诺。此时doSomeAsyncTask()
已经开始执行。为了获得promise的已解析值,我们使用await关键字,这将告诉JavaScript不立即执行下一行,而是等待promise解析然后执行下一行。
如何摆脱异步/等待地狱?
你应该按照这些步骤来逃避异步/等待地狱。
查找依赖于其他语句执行的语句
在我们的第一个例子中,我们选择了比萨饼和饮料。我们的结论是,在选择比萨饼之前,我们需要有比萨饼列表。在将比萨饼添加到购物车之前,我们需要选择比萨饼。所以我们可以说这三个步骤相互依赖。在完成之前的事情之前,我们不能做一件事。
但是如果我们更广泛地看一下,我们发现选择披萨不依赖于选择饮料,所以我们可以同时选择它们。这是机器可以做得比我们做得更好的一件事。
因此,我们发现了一些依赖于其他语句执行的语句,而另一些语句则没有。
异步函数中依赖于组的语句
正如我们所看到的,选择披萨涉及依赖性陈述,例如获取比萨饼列表,选择一个比萨饼,然后将所选比萨饼添加到购物车中。我们应该在异步函数中对这些语句进行分组。这样,我们得到两个异步函数,selectPizza()
和selectDrink()
。
同时执行这些异步功能
然后,我们利用事件循环同时运行这些异步非阻塞函数。这样做的两种常见模式是早期返回Promise和Promise.all方法。
我们来修复这些例子吧
按照这三个步骤,让我们将它们应用于我们的示例。
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
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call
现在我们将语句分为两个函数。在函数内部,每个语句都依赖于前一个语句的执行。然后我们同时执行两个函数selectPizza()
和selectDrink()
。
在第二个例子中,我们需要处理未知数量的承诺。处理这种情况非常简单:我们只需创建一个数组并推送其中的promises。然后使用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
}
// Although I prefer it this way
async function orderItems() {
const items = await getCartItems() // async call
const promises = items.map((item) => sendRequest(item))
await Promise.all(promises) // async call
}
我希望本文能帮助您了解async / await的基础知识,并帮助您提高应用程序的性能。
最后
这篇文章主要传递实现,编程语言不重要。
原文:
https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c