[譯文]如何避開 async/await 地獄

7 人贊了文章

原文地址 How to escape async/await hell

async/await把我們從回調地獄中解放了出來,但是,人們也對其頗有微詞.因為隨之而來導致了async/await地獄的誕生.

在這篇文章,我會試圖解釋什麼是async/await地獄,另外我也會分享一些避開它們的方法.

什麼是 async/await 地獄?

當我們在編寫JavaScript非同步代碼的時候,人們經常在一個接著一個的函數調用前面添加await關鍵字.這會導致性能問題,因為在通常情況下,一個語句的執行並不依賴前一個語句的執行,但是因為添加了await關鍵字,你仍舊需要等待前一個語句執行完才能執行一個語句.

一個 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})()

從表面上看,這段代碼語法是正確的,並且能夠運行.但是,這並不是一個好的實現,因為它剔除了並發執行.接下來讓我們了解一下這段代碼是做什麼的,這樣我們更加明確其中的問題所在.

解釋

我們把這段代碼包裹在了一個非同步的立即執行函數裡面.下面的事情會按次序發生:

  1. 獲得披薩的列表.
  2. 獲得飲料的列表.
  3. 從披薩列表中選擇披薩.
  4. 從飲料列表中選擇飲料.
  5. 把選擇的披薩加入購物車
  6. 把選擇的飲料加入購物車.
  7. 確認訂單

錯誤在哪裡?

就像我在前面提到的那樣,所有的語句都是一行接著一行執行的,這裡不存在並發執行的情況.讓我們仔細想想,為什麼我們在獲取飲料列表之前需要等待披薩列表的返回?我們應該嘗試同時獲取飲料和披薩的列表.然而,當我們需要選擇披薩的時候,我們需要先獲取披薩的列表.飲料也是如此.

因此我們確定,披薩相關的工作和飲料相關的工作能夠同時執行,但是披薩相關的每一步工作需要按次序執行.(順序執行)

另外一個壞例子

這段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()執行完成.然而,實際上我們不需要等待.我們希望儘可能快的發送所有請求然後等待他們都執行完成.

我希望你現在能夠清晰的理解什麼是async/await地獄以及它們對你程序的性能影響有多嚴重.現在,我想問你一個問題

如果我們忘記了await關鍵字會怎樣?

如果你忘記在非同步函數調用的前面添加await關鍵字,這時函數開始執行了,這意味著await並不是函數執行的必要條件.這個非同步函數會返回一個promise,這個promise我們可以在之後使用.

(async () => { const value = doSomeAsyncTask() console.log(value) // an unresolved promise})()

結果就是,編譯器不知道你需要等待這個函數執行完成,因此編譯器會在這個非同步任務還沒有完成的時候退出這個程序.因此我們需要await關鍵字.

promise有一個有趣的性質是你可以在前面的代碼得到這個promise, 然後在後面的代碼中等待這個promise完成.這是從async/await地獄中解脫的關鍵.

(async () => { const promise = doSomeAsyncTask() const value = await promise console.log(value) // the actual value})()

正如你所見到的那樣,doSomeAsyncTask()返回了一個promise.這個時候,doSomeAsyncTask()已經開始執行了.為了得到這個promise的結果值,我們可以在這個promise前面添加await,JavaScript將會立刻停在這裡不再執行下一行代碼,直到獲得了這個promise的返回值,再執行下一行代碼.

如果逃離 async/await 地獄?

你應該跟隨以下步驟來逃離async/await地獄.

找出所有以來其他語句執行的語句

在我們第一個例子裡面,我們在選擇披薩和飲料.因而我們得出結論,在選擇披薩之前,我們需要獲得披薩的列表.同時,在把披薩加入到購物車之前,我們需要選擇披薩.可以認為這三個步驟是互相依賴的,我們不能在前一個步驟完成之前執行下一個任務. 但是,如果我們拓寬一下眼界,就會發現選擇披薩並不會依賴於選擇飲料,我們可以同時選擇他們.這就是機器能做的比我們更好的地方. 至此,我們已經發現了一些語句依賴於其他的語句執行,但是另外一些語句不依賴.

把相互依賴執行的語句整合在非同步函數裡面.

正如我們所看到的,選擇披薩需要幾個互相依賴的語句,如獲得披薩列表,選擇其中一個披薩然後添加到購物車中.我們應該把這些語句整合在一個非同步函數裡面.這樣我們將會得到兩個非同步函數,selectPizza()selectDrink()

並發的執行這些非同步函數.

我們將利用event loop的優勢來並發執行這些非阻塞非同步函數.為了達成這個目標,我們常用的方法是先返回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})()// 我更喜歡下面這種實現.(async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call})()

現在我們已經把這些語句整合到兩個函數中,在每一個函數裡面,每一個語句的執行依賴於前一個函數的執行.然後我們並發的執行selectPizza()selectDrink().

在第二個例子裡面,我們需要解決未知數量的promise.解決這種情況非常簡單:我們只需要創建一個數組然後把promise存入其中.然後使用Promise.all()方法,就能夠並發的等待所有的promise返回結果.

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}// 我更喜歡下面這種實現 async function orderItems() { const items = await getCartItems() // async call const promises = items.map((item) => sendRequest(item)) await Promise.all(promises) // async call}

我喜歡這篇文章能夠幫助你脫離async/await基礎使用者的行列,同時能夠幫助你提高你的程序性能. 如果你喜歡這篇文章,希望能夠點贊並收藏.


推薦閱讀:
相关文章