[譯] 如何逃離 async/await 地獄

Colafornia發表於2018-05-07

[譯] 如何逃離 async/await 地獄

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 立即執行函式 中。準確的執行順序如下:

  1. 獲取披薩列表。
  2. 獲取飲料列表。
  3. 從列表中選擇一份披薩。
  4. 從列表中選擇一杯飲料。
  5. 將選中披薩加入購物車。
  6. 將選中飲料加入購物車。
  7. 將購物車內物品下單。

哪裡出問題了?

如我之前所強調過的,所有語句都會逐一執行。此處並無併發操作。仔細想一下:為什麼我們要在獲取披薩列表完成後才去獲取飲料列表呢?兩個列表應該一起獲取。但是我們在選擇披薩時,確實需要在這之前已獲取飲料列表。飲料同理。

因此,我們可以總結出,披薩相關的事務與飲料相關事務可以併發發生,但是披薩相關事務內部的獨立步驟需要繼發進行(逐一進行)。

另一個糟糕實現的例子

這段程式碼將獲取購物車內的東西,併發起一個請求下單。

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。如果想獲取文章更新,可以在 TwitterMedium 上關注我。有任何問題可以在評論中指出。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章