async/await让我们摆脱了回调地狱,但人们开始滥用它——导致了async/await地狱的诞生。
在本文中,我将尝试解释async/await地狱是什么,并分享一些逃离它的技巧。
什么是async/await地狱
在处理异步JavaScript时,人们经常在一个函数调用前添加一个await,然后连续写多个语句。这会导致性能问题,因为很多时候一个语句并不依赖于前一个语句,但你仍然必须等待前一个语句完成。
async/await地狱的一个例子
考虑你写了一个脚本来订购一份比萨和一杯饮料。这个脚本可能长这样:
表面上看起来是正确的,而且也能正常工作。但这并不是一个好的实现,因为它忽略了并发。让我们理解一下它在做什么,以便我们能找到问题。
解释
我们将代码放在一个async中。以下内容按照以下确切的顺序发生:
- 获取披萨列表。
- 获取饮料列表。
- 从列表中选择一份披萨。
- 从列表中选择一杯饮料。
- 将所选披萨添加到购物车。
- 将所选饮料添加到购物车。
- 订单中的物品。
那么问题在哪里?
正如我之前强调的,所有这些语句都是一个接一个地执行的。这里没有并发。仔细想想:为什么我们要等待获取比萨列表才尝试获取饮料列表?我们应该尝试同时获取两个列表。然而,当我们需要选择比萨时,我们确实需要先有比萨列表。饮料也是一样的。
因此,我们可以得出结论:与比萨相关的工作和与饮料相关的工作可以并行进行,但涉及到比萨相关工作的个别步骤需要按顺序逐个进行。
另一个不好的实现例子
这个JavaScript代码片段将获取购物车中的物品并发出订单请求。
在这种情况下,for循环必须等待sendRequest()
函数完成后才能继续下一次迭代。然而,我们实际上不需要等待。我们想尽快发送所有请求,然后等待它们全部完成。
我希望现在你已经更接近理解async/await地狱是什么以及它如何严重影响你程序的性能了。现在我想问你一个问题。
如果我们忘记了await关键字会怎样?
如果在调用异步函数时忘记使用await,函数将开始执行。这意味着执行函数时不需要等待。异步函数将返回一个promise,稍后可以使用它。
另一个后果是编译器不会知道你想等待函数完全执行。因此,编译器将退出程序而不完成异步任务。因此我们确实需要await关键字。
promise的一个有趣的特性是你可以在一行代码中获取一个promise,在另一行中等待它解决。这是逃离async/await地狱的关键。
正如你所看到的,doSomeAsyncTask()
正在返回一个promise。此时,doSomeAsyncTask()
已经开始执行。要获取promise的解决值,我们使用await关键字,这会告诉JavaScript不要立即执行下一行,而是等待promise解决后再执行下一行。
如何摆脱async/await地狱?
你应该遵循以下步骤来逃离async/await地狱。
找到依赖于其他语句执行的语句
在我们的第一个例子中,我们选择了一份比萨和一杯饮料。我们得出结论,在选择比萨之前,我们需要有比萨列表。在将比萨添加到购物车之前,我们需要选择比萨。因此,我们可以说这三个步骤彼此依赖。我们无法在完成前一个任务之前执行另一个任务。
但是,如果我们更广泛地看待它,我们会发现选择比萨不依赖于选择饮料,因此我们可以并行选择它们。这是机器比我们做得更好的一件事。
因此,我们发现了一些依赖于其他语句执行的语句以及一些不依赖于其他语句执行的语句。
将依赖语句分组到异步函数中
正如我们所看到的,选择比萨涉及到依赖语句,如获取比萨列表、选择比萨,然后将所选比萨添加到购物车中。我们应该将这些语句分组到一个异步函数中。这样我们就得到了两个异步函数selectPizza()
和selectDrink()
。
并发执行这些异步函数
然后,我们利用事件循环来同时运行这些异步非阻塞函数。完成这个有两个常见的模式:提前返回promise和Promise.all方法。
让我们修复这些例子
遵循这三个步骤,让我们将它们应用于我们的例子。
现在,我们已将语句分组为两个函数。在函数内部,每个语句都依赖于前一个语句的执行。然后我们同时执行selectPizza()
和selectDrink()
这两个函数。
在第二个例子中,我们需要处理未知数量的promise。处理这种情况非常容易:我们只需创建一个数组并将promise推入其中。然后使用Promise.all()
我们可以同时等待所有promise解决。
我希望这篇文章帮助你超越async/await的基础知识,并帮助你提高应用程序的性能。
评论(0)