- 原文地址:How to escape async/await hell
- 原文做者:Aditya Agarwal
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Colafornia
- 校对者:Starriers whuzxq
async/await 将咱们从回调地狱中解脱,但人们的滥用,致使了 async/await 地狱的诞生。javascript
本文将阐述什么是 async/await 地狱,以及逃离 async/await 地狱的几个方法。前端
进行 JavaScript 异步编程时,你们常常须要逐一编写多个复杂语句的代码,并都在调用语句前标注了 await。因为大多数状况下,一个语句并不依赖于前一个语句,可是你仍不得不等前一个语句完成,这会致使性能问题。java
思考一下,若是你须要写一段脚本预订一个披萨和一杯饮料。脚本可能会是这样的:android
(async () => {
const pizzaData = await getPizzaData() // 异步调用
const drinkData = await getDrinkData() // 异步调用
const chosenPizza = choosePizza() // 同步调用
const chosenDrink = chooseDrink() // 同步调用
await addPizzaToCart(chosenPizza) // 异步调用
await addDrinkToCart(chosenDrink) // 异步调用
orderItems() // 异步调用
})()
复制代码
表面上看起来没什么问题,这段代码也能够执行。可是它并非一个好的实现,由于它没有考虑并发性。让咱们了解一下这段代码是怎么运行的,这样才能够肯定问题所在。ios
咱们将这段代码包裹在一个异步的 IIFE 当即执行函数 中。准确的执行顺序以下:git
如我以前所强调过的,全部语句都会逐一执行。此处并没有并发操做。仔细想一下:为何咱们要在获取披萨列表完成后才去获取饮料列表呢?两个列表应该一块儿获取。可是咱们在选择披萨时,确实须要在这以前已获取饮料列表。饮料同理。github
所以,咱们能够总结出,披萨相关的事务与饮料相关事务能够并发发生,可是披萨相关事务内部的独立步骤须要继发进行(逐一进行)。编程
这段代码将获取购物车内的东西,并发起一个请求下单。后端
async function orderItems() {
const items = await getCartItems() // 异步调用
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // 异步调用
}
}
复制代码
在这种状况下,for 循环须要等待 sendRequest()
函数完成后才能进行下一个迭代。事实上,咱们不须要等待。咱们想要尽快发送全部请求,而后等待全部请求执行完毕。数组
但愿如今你能够更理解 async/await 地狱是什么,以及它对你的程序性能影响有多么严重。如今,我想问你一个问题。
若是你在调用一个异步函数时忘了使用 await 关键字,该函数就会当即开始执行。这意味着 await 对于函数的执行来讲不是必需的。异步函数会返回一个 promise 对象,你能够稍后使用这个 promise。
(async () => {
const value = doSomeAsyncTask()
console.log(value) // 一个未完成的 promise
})()
复制代码
不使用 await 调用异步函数的另外一个后果是,编译器不知道你想等待这个函数执行完成。所以编译器将在异步任务完成以前就退出程序。所以咱们确实须要 await 关键字。
promise 有一个好玩的特性,你能够在一行代码中获得一个 promise 对象,在另外一行代码中获得这个 promise 的执行结果。这是逃离 async/await 地狱的关键。
(async () => {
const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // 实际的返回值
})()
复制代码
如你所见,doSomeAsyncTask()
返回了一个 promise 对象。此时 doSomeAsyncTask()
开始执行。咱们使用 await 关键字来获取 promise 对象的执行结果,并告诉 JavaScript 不要当即执行下一行代码,而是等待 promise 执行完成再执行下一行代码。
你须要遵循如下步骤:
在第一个示例中,咱们选择了一份披萨和一杯饮料。能够推断出在选择一份披萨前,咱们须要先得到全部披萨的列表。在将选择的披萨加入购物车以前,咱们须要先选择一份披萨。所以咱们能够说这三个步骤是互相依赖的。咱们不能在前一件事完成以前作下一件事。
可是若是把问题看得更普遍一些,咱们能够发现选披萨并不依赖选饮料,所以咱们能够并行选择。这方面,机器能够比咱们作的更好。
所以咱们已经发现有一些语句依赖于其它语句的执行,有些则不依赖。
如咱们所见,选择披萨包括了如获取披萨列表,选择披萨,将所选披萨加入购物车等依赖语句。咱们应该将这些语句包裹在一个 async 函数中。这样咱们获得了两个 async 函数,selectPizza()
和 selectDrink()
。
而后咱们能够利用事件循环并发执行这些非阻塞 async 函数。有两种经常使用模式,分别是优先返回 promises 和使用Promise.all 方法。
遵循如下三个步骤,将它们应用到咱们的示例中。
async function selectPizza() {
const pizzaData = await getPizzaData() // 异步调用
const chosenPizza = choosePizza() // 同步调用
await addPizzaToCart(chosenPizza) // 异步调用
}
async function selectDrink() {
const drinkData = await getDrinkData() // 异步调用
const chosenDrink = chooseDrink() // 同步调用
await addDrinkToCart(chosenDrink) // 异步调用
}
(async () => {
const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // 异步调用
})()
// 我更喜欢这种方法
(async () => {
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // 异步调用
})()
复制代码
如今咱们将语句分组到两个函数中。在函数内部,每一个语句依赖于前一个语句的执行。而后咱们并发执行两个函数 selectPizza()
和 selectDrink()
。
在第二个例子中,咱们须要处理未知数量的 promise。解决这种状况很容易:建立一个数组,将 promise push 进去。而后使用 Promise.all()
咱们就能够并行等待全部的 promise 处理完毕。
async function orderItems() {
const items = await getCartItems() // 异步调用
const noOfItems = items.length
const promises = []
for(var i = 0; i < noOfItems; i++) {
const orderPromise = sendRequest(items[i]) // 异步调用
promises.push(orderPromise) // 同步调用
}
await Promise.all(promises) // 异步调用
}
复制代码
但愿本文能够帮你提升 async/await 的基础水平并提高应用的性能。
若是喜欢本文,请点个喜欢。
也请分享到 Fb 和 Twitter。若是想获取文章更新,能够在 Twitter 和 Medium 上关注我。有任何问题能够在评论中指出。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。