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

YusenMeng發表於2018-09-16

原文地址 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基礎使用者的行列,同時能夠幫助你提高你的程式效能.如果你喜歡這篇文章,希望能夠點贊並收藏.

來源:https://juejin.im/post/5b9db6925188255c3b7d78cb

相關文章