這篇文章主要關注 async/await 的執行原理。雖然我之前也有用過 async/await,但場景都比較簡單,對它也只是有個大體的理解,近期作了一個 node 服務工程開始大量使用 async/await,在使用的細節上產生了不少疑惑。好比:html
await Promise 經過在外面 try catch 和給 Promise 加 .catch() 均可以處理異常,那咱們應該用哪一種?node
下面的 async 函數被執行時,若是沒有聲明 await 爲何就不能 catch 到 _run 方法內部的異常了?c#
async run () {
try {
await _run()
} catch (err) {}
}
複製代碼
async 函數裏的 try catch 能捕獲哪些異常?爲何說它比 Promise 的異常處理好?promise
async 函數若是沒有用到 await 會怎麼樣,或者說若是我把一個普通函數給加上 async 定義,還能像從前同樣用它嗎?bash
async 函數若是直接 return 值 'great',就會至關於 return 一個 resolve 值是 great 的 Promise,若是直接 return 一個 resolve 值是 great 的 Promise,實際 await 到的值依然是 great,而不是這個 Promise。爲何?若是 return 的是一個尚未 resolve 的 Promise,會怎麼樣?這裏的原理是怎樣的?ecmascript
帶着這些疑問,通過一番查找,我逐漸明確了它的實現原理,明白了爲何人們常說它只是 Promise 的語法糖,而實際上它是藉助 generator 來更清晰地表達 Promise 的異步流程,這篇回答 能夠說明這一點。而 這篇 tc39 的草案 是我認爲可以比較清晰地解釋 async/await 執行原理的材料,貼一下代碼:異步
function spawn(genF, self) {
return new Promise(function(resolve, reject) {
var gen = genF.call(self);
function step(nextF) {
var next;
try {
next = nextF();
} catch(e) {
// finished with failure, reject the promise
reject(e);
return;
}
if(next.done) {
// finished with success, resolve the promise
resolve(next.value);
return;
}
// not finished, chain off the yielded promise and `step` again
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
複製代碼
既然是藉助了 generator,那想要理解這段代碼,就要先熟悉 generator 的特性,這裏就不詳細介紹了,有不熟悉的朋友建議先弄清楚。這個函數的做用就是將 async 函數轉化成一個會 return Promise 的新函數,Promise 的執行過程會將原函數做爲 generator 自動循環執行,執行的三個關鍵點是:async
建議你們先理解代碼,嘗試本身回答一下開篇的幾個問題,而後再繼續往下看個人解釋。函數
關於異常處理的:工具
第一個問題,.catch 是 Promise 自己就支持的寫法,咱們用它處理也沒有問題,只是既然都用了 async/await 就最好保持風格一致,都用 try catch 來處理比較好。
第二個問題,若是沒有聲明 await 爲何就不能 catch 到 _run 方法內部的異常了?咱們能夠帶入一下上面的原理代碼,若是沒有加 await 直接就會走到 next.done 結束掉,而帶上 await 則會走到 Promise.resolve(next.value).then 的部分,這個 next.value 就是 _run() 返回的 Promise,這樣 _run() 被 reject 時就會進入到 step(function() { return gen.throw(e); }) 於是捕捉到異常。
第三個問題,爲何說它比 Promise 的異常處理好?經過上面的原型代碼,咱們能夠看到 async 函數能夠經過 try { next = nextF(); } 捕獲各層次代碼中的語法錯誤等普通異常,也能夠經過 Promise.resolve(next.value).then 的 onRejected 回調中的 gen.throw(e) 來捕捉 await 的 Promise 被 reject 的狀況,異常的結果都是致使 async 函數做爲 Promise 的總體被 reject。而對 Promise 包裹 try catch 是不能捕獲 then 回調鏈中的普通異常的。
關於 async 函數的:
第一個問題,若是我把一個普通函數給加上 async 定義,還能像從前同樣用它嗎?看了上面的代碼後,答案已經比較明確了,加上了 async 以後它就變了,返回的值會是一個 Promise,至於能不能像之前同樣用它,就得看狀況了,若是它之前就是返回了一個 Promise,那你極可能仍是能夠保持它的使用方式的。
第二個問題是關於 async 函數的 return 值,看上面代碼 —— if (next.done) { resolve(next.value) } ,這其實就只是一個 Promise 的原理問題,是當 Promise a 被 resolve(x) 時,x 若是是 Promise,那 a 的回調就會被掛到 x 上,咱們 await a 也就至關於 await x。
要理解上面的內容,關鍵仍是要先理解 Promise 的原理,若是有不清楚,建議再仔細看看 Promise 規範,特別是 定義 then 的部分 和 The Promise Resolution Procedure。Then 的規範定義了 then 方法必須 return 一個 promise,而 await 後面的語句其實就是 then 方法中的 onFullfilled,於是 async 函數會 return 一個新的 promise,這個 promise 的行爲也是遵循這個規範的。而 The Promise Resolution Procedure 是對如何處理 resolve 值的要求,這跟不少細節有關,好比上面提到的 async 函數 return 值,以及下面即將提到的 v8 團隊對 async/await 的改造。在弄清了這兩部分規範以後 async/await 也就容易理解了。
網上關於 async/await 比較有價值的一篇文章是 v8 團隊對 async/await 的改進,主要是介紹了在 2018 年對 async/await 實現方式的改進,減小了一個對 Promise 的包裝和傳遞過程, 這裏 有對此比較詳細的解釋,只是把 new Promise(res => res(p)) 改爲了 Promise.resolve(p) 就節省了兩個 microTick。除此以外改進還減小了一個新建 throw away Promise 的過程,也就是在 then 的時候不返回新的 Promise,節省建立這個 Promise 的時間,不過這個並無減小 microTick。在此次改良後 await 在執行順序上就比較符合人們直觀的預期了。
從幾年前的 Async/Await 完勝 Promise 的六個理由 到 如何避免 async/await 地獄,關於 Async/await 的討論還在繼續。我的感受 Async/await 是一個更好的工具,它讓多層嵌套的複雜異步邏輯更直觀、清晰,可是它對寫代碼的人提出了更高的要求,若是沒有足夠的瞭解,可能更容易寫出 Bug。