- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。