原文地址:How to escape async/await hell
譯文出自:夜色鎮歌的我的博客javascript
async/await 把咱們從回調地獄中解救了出來,可是若是濫用就會掉進 async/await 地獄。java
本文中我會解釋一下什麼是 async/await 地獄,並會分享幾個技巧去避免。編程
異步 Javascript 編程中,咱們一般會寫許多 async 方法,而且使用 await
關鍵字去等待它,有不少時候下一行的執行並不依賴於上一行,可是咱們仍然使用了 await
去等待,因此可能會致使一些性能問題。數組
如何編寫一個訂購披薩和飲料的代碼?它可能會像這樣: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
包裹了起來,而後下面這些會依次執行。異步
正如我剛強調的,這些語句會依次執行,沒有併發。仔細想一下,爲啥我獲取飲料菜單以前得先獲取披薩菜單?這兩份菜單我應該同時去獲取。固然,選擇披薩以前得先獲取披薩菜單,這個規則一樣適用於飲料。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,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 地獄:
第一個例子中,咱們選擇了一個披薩和一杯飲料。總結一下,選擇披薩以前得先獲取披薩菜單,加到購物車以前得先選好,這三個步驟都是相互依賴的,必須等待上一個步驟完成後才能進行下一步。
咱們選擇飲料的時候並不依賴於選擇披薩,因此選擇披薩和飲料是能夠並行執行的。這也是機器能比咱們作的更好的一件事。
正如你看到的,選擇披薩的依賴有獲取披薩菜單、選擇、添加到購物車。因此咱們把這些依賴放在一個異步方法裏,飲料同理,這也是爲何咱們會有 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 使用的思考,也但願能幫助你提高程序的性能。