效能優化小冊 - 非同步堆疊追蹤:為什麼 await 勝過 Promise

Leiy發表於2020-05-12

與直接使用 Promise 相比,使用 async/await 不僅可以使程式碼更具可讀性,而且還可以在 JavaScript 引擎中實現一些有趣的優化。

這篇文章是關於一個這樣的優化,涉及非同步程式碼的堆疊追蹤。

async/awaitPromise 的根本區別在於 await fn() 暫停當前函式的執行,而 promise.then(fn) 在將 fn 呼叫新增到回撥鏈後,繼續執行當前函式。

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 引擎都會顯示一條錯誤資訊和(希望)記錄一個有用的堆疊追蹤。

作為一名開發人員,無論您使用的是普通的 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 的非同步呼叫解析時,上下文已經消失了。

想象一下如果 b(或 c )非同步丟擲異常會發生什麼?理想情況下,堆疊追蹤應該包括 a,因為 b(或 c)是從那裡呼叫的,對吧?既然我們不在參考 a 了 ,那怎樣能做到呢?

為了讓它工作,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 傳輸。

相關文章