性能優化小冊 - 異步堆棧追蹤:爲何 await 賽過 Promise

與直接使用 Promise 相比,使用 async/await 不只能夠使代碼更具可讀性,並且還能夠在 JavaScript 引擎中實現一些有趣的優化。html

這篇文章是關於一個這樣的優化,涉及異步代碼的堆棧追蹤。promise

async/awaitPromise 的根本區別在於 await fn() 暫停當前函數的執行,而 promise.then(fn) 在將 fn 調用添加到回調鏈後,繼續執行當前函數。babel

const fn = () => console.log('hello')
const a = async () => {
  await fn() // 暫停 fn 的執行
}
// 調用 a 時,才恢復 fn 的執行
a() // "hello"

const promise = Promise.resolve()
// 將 fn 添加到回調鏈後,繼續執行 fn
promise.then(fn) // "hello"

在堆棧追蹤的上下文中,這種差別很是顯著。異步

當一個 Promise 鏈(不管是否脫糖化)在任什麼時候候拋出一個未經處理的異常時, JavaScript 引擎都會顯示一條錯誤信息和(但願)記錄一個有用的堆棧追蹤。async

做爲一名開發人員,不管您使用的是普通的 Promise 仍是 async await,您都會指望這樣。函數

Promise

想象一個場景,當對異步函數 b 的調用解析時,調用函數 c性能

const b = () => Promise.resolve()
const a = () => {
    b().then(() => c())
}

當調用 a 時,將同步發生如下狀況:優化

  • b 被調用並返回一個 Promise,該 Promise 將在未來某個時刻解決。
  • .then 回調(其實是調用 c())被添加到回調鏈中( V8 術語中,[…]被添加爲解析處理程序)。

以後,咱們完成了在函數 a 的主體中執行代碼。a 永遠不會被掛起,當對 b 的異步調用解析時,上下文已經消失了。code

想象一下若是 b(或 c )異步拋出異常會發生什麼?理想狀況下,堆棧追蹤應該包括 a,由於 b(或 c)是從那裏調用的,對吧?既然咱們不在參考 a 了 ,那怎樣能作到呢?htm

爲了讓它工做,JavaScript 引擎須要在上面的步驟以外作一些事情:它在有機會的時候捕獲並存儲堆棧追蹤。

V8 中,堆棧追蹤附加到 b 返回的 Promise。當 Promise 實現時,堆棧追蹤將被傳遞,以便 c 能夠根據須要使用它。

b()[a] -> b().then()[a] -> c[a?:a]

捕獲堆棧追蹤須要時間(即下降性能);存儲這些堆棧追蹤須要內存。

async/await

下面是一樣的程序,使用 async/await 而不是 Promise 編寫:

const b = () => Promise.resolve()
const a = async () => {
  await b()
  c()
}

使用 await,即便在 await 調用中不收集堆棧追蹤,咱們也能夠恢復調用鏈。

這是可能的,由於 a 被掛起,正在等待 b 解決。若是 b 拋出異常,則能夠按需以這種方式重建堆棧追蹤。

若是 c 拋出異常,堆棧追蹤能夠像同步函數那樣構造,由於發生這種狀況時,咱們仍在 a 上下文中。

經過遵循如下建議,使 JavaScript 引擎可以以更高效的方式處理堆棧追蹤:

  • 偏好 async/await 賽過 Promise
  • 使用 @babel/preset env 避免沒必要要的 async/await 傳輸。
相關文章
相關標籤/搜索