async 實現原理分析
一、簡要概述
async 函數,是一種對異步函數更加優雅的處理方式,本質是 Generator 函數的語法糖。所以為了更好的閱讀本博客,建議對以下知識點有所了解:
- 遍歷器對象(Generator 函數的返回值是一個遍歷器對象)
- Generator 函數基礎語法(函數定義、yield 表達式、yield * 表達式)
- Generator.prototype.return 和 Generator.prototype.throw
async 函數與 Generator 函數不同在於下面幾點:
- async 內置執行器,不用像 Generator 函數那樣去反復調用返回遍歷器的 next 方法
- 語義更好:async 表示異步,await 表示等待異步結果
- await 后既可以是 Promise 對象,也可以是原始類型的值
- async 函數的返回值不是遍歷器對象,而是一個 Promise 對象,
二、基礎語法
正常使用:
let a = async () => {
let a = await Promise.resolve(1); // await 將后面 Promise 實例的值返回
let b = await 2;
return a + b;
}
a().then(res=>console.log(res))
// a() 返回一個 promise 實例,
// a 函數里 return 的值會作為成功回調參數
錯誤處理:
let a = async () => {
await Promise.reject(1);
console.log('這里不會執行');
// await 后面的 Promise 變為 reject,整個 async 函數的 Promise 實例也會 reject,並且會中斷函數后面的執行
}
a().catch(err=>console.log(err));
三、實現原理
async 函數的實現原理就是將 Generator 函數和自動執行器包裝在一個函數里。
async function fn(args)=> {
...
}
// 等同於
function fn(args) {
return spawn(function * () {
...
})
}
spawn 函數的實現如下:
function spawn(genF) {
return new Promise((resolve, reject)=>{
// genF 是 Generator 函數,gen 是生成的遍歷器對象
let gen = genF();
// 定義分步函數
let step = (nextF) => {
/* async 函數的每一步,都是用 try catch 執行的,一旦有錯,立刻 reject */
try {
let next = nextF();
} catch(err) {
return reject(e);
}
/* async 執行完最后一步,return 的值會作為成功回調參數 */
if (next.done) {
return resolve(next.value);
}
/* asyc 還沒有執行完,await 后值存放在 next.value
將 next.value 轉化為 promise 實例,然后指定回調為 step
就可以實現 Generator 函數的自動執行 */
Promise.resolve(next.value).then(
v => {step( ()=>gen.next(v) )}, // v 作為 await 返回值
e => {step( ()=>gen.throw(e) )} // reject 時 await 沒有返回值
)
}
// 開始執行 async 函數
step( ()=>gen.next() );
})
}
async 函數 return 原理:
async 函數本質還是 Generator 函數,是通過不斷執行遍歷器對象的 next 方法來執行函數。當遍歷器對象遍歷完畢,就將最后遍歷 (return) 的值 resolve 出來傳遞給成功回調。
async 函數自動執行原理:
await 有兩個作用:
- 和 yield 一樣,可以將函數的執行進行切割和分步。
- 可以將 await 后面的值轉化為 promise 實例,然后指定它的回調。所以通過將 async 函數的下一步執行指定為這個 promise 實例的回調,就可以實現 async 的自動執行。
await 返回值原理
我們知道, Generator 函數遍歷器的 next 方法的參數,會作為上一個 yield 的返回值。而 await 會指定其后 promise 的成功回調執行遍歷器對象的 next 方法,我們再把 promise 的成功返回值作為 next 方法的參數,這個值就會成為 await 的返回值。
async 出錯處理機制
分兩類:
- async 函數里的語句執行出錯。因為 async 函數的每一步,都是通過 try catch 執行的,所以錯誤都會被捕獲到然后 reject 傳遞給錯誤回調
- await 后的 promsie 實例被 reject 了。這樣會觸發 await 給該 promise 指定的錯誤回調,這個錯誤回調會調用 Generator.prototype.throw 扔一個錯誤到 async 函數里面,如果里面有 try catch 接住就讓它接住,然后繼續執行函數 (此時 await 語句出錯,不會有返回值);如果里面沒有 try,這個 throw 的錯誤會被第一點所說的 try 接住,然后 reject 傳遞到錯誤回調。
/* 1. 錯誤被外部接住 */
(async () => {
await Promise.reject(1); // 這個 await 是無返回值的
// 等同在這里執行了: throw 1
})()
.catch(err => console.log(err));
/* 2. 錯誤被內部接住 */
(async () => {
try {
await Promise.reject(1); // 這個 await 是無返回值的
// 等同在這里執行了: throw 1
} catch(e) {
console.log(e);
}
})()
.catch(err => console.log(err));
五、拓展
let b = async ()=> {
Promise.reject(22);
}
b().then(null, err=>console.log(err));
上面這個例子,會直接報錯,不會捕捉到錯誤然后執行錯誤回調。
這是為什么呢?
首先我們要知道 Promise 觸發錯誤回調的前提:執行 reject 函數。
而執行 reject 函數,有兩個前提:
- 開發者調用了 reject 函數
- try catch 執行函數的時候捕獲到錯誤,將錯誤 reject 出來。
其中,第一個是開發者可見,第二個是源碼底層實現,開發者不可見。
比如這個例子
new Promise(()=>{
throw 1;
}).catch(err=>console.log(err))
// 異步函數其實會被 try catch 執行,一旦捕獲到錯誤,就 reject 掉
try {
throw 1
} catch (err) {
reject(err)
}
// 而對於下面這個例子,錯誤是根本捕捉不到的
try {
Promise.reject(22);
} catch (err) {
reject(err)
}
所以我們知道,promise 真正捕獲到錯誤觸發錯誤回調,關鍵還是在於 reject 函數是否有執行,而不是函數內部是否真的發生了錯誤。
回到開始,對於這個例子:
let b = async ()=> {
Promise.reject(22);
}
b().then(null, err=>console.log(err));
而在剛剛介紹 async 函數實現原理的時候,我們知道 async 函數在執行每一步的時候,也是用 try catch 去執行的:
try {
Promise.reject(22);
} catch (err) {
reject(err)
}
剛剛我們已經說明過了,這樣子是捕捉不到錯誤的。