與直接使用 Promise
相比,使用 async/await
不僅可以使程式碼更具可讀性,而且還可以在 JavaScript 引擎中實現一些有趣的優化。
這篇文章是關於一個這樣的優化,涉及非同步程式碼的堆疊追蹤。
async/await
和 Promise
的根本區別在於 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
傳輸。