如何避免 await/async 地獄

img

原文地址:How to escape async/await hell
譯文出自:夜色鎮歌的我的博客javascript

async/await 把咱們從回調地獄中解救了出來,可是若是濫用就會掉進 async/await 地獄。java

本文中我會解釋一下什麼是 async/await 地獄,並會分享幾個技巧去避免。編程

啥是 await/async 地獄

異步 Javascript 編程中,咱們一般會寫許多 async 方法,而且使用 await 關鍵字去等待它,有不少時候下一行的執行並不依賴於上一行,可是咱們仍然使用了 await 去等待,因此可能會致使一些性能問題。數組

一個 await/async 地獄的例子

如何編寫一個訂購披薩和飲料的代碼?它可能會像這樣:promise

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

看起來沒什麼問題,也能正常工做,但這並非一個好的實現。先來看下這段代碼都作了什麼,以便定位問題。併發

解釋下

咱們把代碼用 async IIFE 包裹了起來,而後下面這些會依次執行。異步

  1. 獲取披薩菜單
  2. 獲取飲料菜單
  3. 從披薩菜單中選擇披薩
  4. 從飲料菜單中選擇飲料
  5. 把選好的披薩加到購物車
  6. 把選好的飲料加到購物車
  7. 下單

哪裏錯了?

正如我剛強調的,這些語句會依次執行,沒有併發。仔細想一下,爲啥我獲取飲料菜單以前得先獲取披薩菜單?這兩份菜單我應該同時去獲取。固然,選擇披薩以前得先獲取披薩菜單,這個規則一樣適用於飲料。async

因此咱們能夠得出結論,披薩相關的工做和飲料相關的工做能夠並行進行,但涉及披薩相關工做的各個步驟須要按順序進行(一步接着一步)。函數

另外一個糟糕的例子

這段代碼會獲取購物車中的購物項而且發出訂購請求。性能

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,async 函數會執行而且返回一個 Promise,你能夠稍後再去resolve。

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

另外一個後果是編譯器不知道你想把函數徹底執行,因此編譯器會退出程序而不完成異步函數,因此仍是須要使用 await 關鍵字

promises 一個有趣的特性就是你能夠在一行代碼中去獲得 Promise ,而在另一行中去等待並 resolve,這是避免 async/await 地獄的關鍵之處。

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

正如你看到的,doSomeAsyncTask() 方法返回一個 Promise,調用的時候它已經開始執行了,爲了獲得他的解析值,咱們使用了 await 關鍵字,告訴編譯器等待解析完畢再執行下一行。

如何避免 async/await 地獄

你應該按照這些步驟來避免 async/await 地獄:

找到語句的依賴關係

第一個例子中,咱們選擇了一個披薩和一杯飲料。總結一下,選擇披薩以前得先獲取披薩菜單,加到購物車以前得先選好,這三個步驟都是相互依賴的,必須等待上一個步驟完成後才能進行下一步。

咱們選擇飲料的時候並不依賴於選擇披薩,因此選擇披薩和飲料是能夠並行執行的。這也是機器能比咱們作的更好的一件事。

封裝相互依賴的異步方法

正如你看到的,選擇披薩的依賴有獲取披薩菜單、選擇、添加到購物車。因此咱們把這些依賴放在一個異步方法裏,飲料同理,這也是爲何咱們會有 selectPizza()selectDrink() 兩個異步方法。

並行執行

咱們利用事件循環去非阻塞並行地執行這些異步方法,一般會用的兩個方法就是儘早的返回 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
})()

// Although I prefer it this way 

(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems)   // async call
})()

咱們把相互依賴的語句封裝在各自的函數裏,如今同時去執行 selectPizza()selectDrink()

第二個例子中,咱們須要處理未知數量的 Promise 。處理這種狀況很簡單,咱們先把 Promises 放進數組,而後使用 Promise.all() 讓他們並行執行,以後等待他們全都執行完畢。

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
}

// Although I prefer it this way

async function orderItems() {
  const items = await getCartItems()    // async call
  const promises = items.map((item) => sendRequest(item))
  await Promise.all(promises)    // async call
}

但願本文能夠引起你對 async/await 使用的思考,也但願能幫助你提高程序的性能。

相關文章
相關標籤/搜索