- 原文地址:How to escape async/await hell
- 原文作者:Aditya Agarwal
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Colafornia
- 校對者:Starriers whuzxq
async/await 將我們從回撥地獄中解脫,但人們的濫用,導致了 async/await 地獄的誕生。
本文將闡述什麼是 async/await 地獄,以及逃離 async/await 地獄的幾個方法。
什麼是 async/await 地獄
進行 JavaScript 非同步程式設計時,大家經常需要逐一編寫多個複雜語句的程式碼,並都在呼叫語句前標註了 await。由於大多數情況下,一個語句並不依賴於前一個語句,但是你仍不得不等前一個語句完成,這會導致效能問題。
一個 async/await 地獄示例
思考一下,如果你需要寫一段指令碼預訂一個披薩和一杯飲料。指令碼可能會是這樣的:
(async () => {
const pizzaData = await getPizzaData() // 非同步呼叫
const drinkData = await getDrinkData() // 非同步呼叫
const chosenPizza = choosePizza() // 同步呼叫
const chosenDrink = chooseDrink() // 同步呼叫
await addPizzaToCart(chosenPizza) // 非同步呼叫
await addDrinkToCart(chosenDrink) // 非同步呼叫
orderItems() // 非同步呼叫
})()
複製程式碼
表面上看起來沒什麼問題,這段程式碼也可以執行。但是它並不是一個好的實現,因為它沒有考慮併發性。讓我們瞭解一下這段程式碼是怎麼執行的,這樣才可以確定問題所在。
解釋
我們將這段程式碼包裹在一個非同步的 IIFE 立即執行函式 中。準確的執行順序如下:
- 獲取披薩列表。
- 獲取飲料列表。
- 從列表中選擇一份披薩。
- 從列表中選擇一杯飲料。
- 將選中披薩加入購物車。
- 將選中飲料加入購物車。
- 將購物車內物品下單。
哪裡出問題了?
如我之前所強調過的,所有語句都會逐一執行。此處並無併發操作。仔細想一下:為什麼我們要在獲取披薩列表完成後才去獲取飲料列表呢?兩個列表應該一起獲取。但是我們在選擇披薩時,確實需要在這之前已獲取飲料列表。飲料同理。
因此,我們可以總結出,披薩相關的事務與飲料相關事務可以併發發生,但是披薩相關事務內部的獨立步驟需要繼發進行(逐一進行)。
另一個糟糕實現的例子
這段程式碼將獲取購物車內的東西,併發起一個請求下單。
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 關鍵字,該函式就會立即開始執行。這意味著 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/await 地獄?
你需要遵循以下步驟:
找到依賴其它語句執行結果的語句
在第一個示例中,我們選擇了一份披薩和一杯飲料。可以推斷出在選擇一份披薩前,我們需要先獲得所有披薩的列表。在將選擇的披薩加入購物車之前,我們需要先選擇一份披薩。因此我們可以說這三個步驟是互相依賴的。我們不能在前一件事完成之前做下一件事。
但是如果把問題看得更廣泛一些,我們可以發現選披薩並不依賴選飲料,因此我們可以並行選擇。這方面,機器可以比我們做的更好。
因此我們已經發現有一些語句依賴於其它語句的執行,有些則不依賴。
將互相依賴的語句包裹在 async 函式中
如我們所見,選擇披薩包括瞭如獲取披薩列表,選擇披薩,將所選披薩加入購物車等依賴語句。我們應該將這些語句包裹在一個 async 函式中。這樣我們得到了兩個 async 函式,selectPizza()
和 selectDrink()
。
併發執行 async 函式
然後我們可以利用事件迴圈併發執行這些非阻塞 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。