文章目錄
基本概念
將async和await看作命令
async/await就是generator函數的語法糖,但對generator函數有部分改進:
- generator函數執行時返回的是一個Iterator對象,而async/await立即返回一個Promise對象,相當於能夠將函數內部的多個異步操作封裝成爲一個Promise對象。
- async/await內部自帶執行器,也就是調用後會自動執行,而generator函數必須手動執行,或者編寫執行器。
- await可以看作內部的then語法糖。
- async函數遇到await時會等待後面的語句(緊跟着的一個語句)執行完成纔會繼續往下執行。此時等待的意思函數會交出執行權,讓主線程去執行其他的代碼,待完成await後的語句時,才能夠獲取執行權繼續向下執行。
async
- 被async修飾的函數執行時會立即返回一個Promise對象。
- 函數中的返回值會作爲Promise的then方法中resolved狀態下的參數。
- 無返回值則是Promise.resolve(undefined)
- 返回值是Promise,則就是返回的該Promise
- 其他情況則是Promise.resolve(other)
- 停止函數執行的情況
- 函數內部任何拋出的錯誤會停止函數的執行(即並不單指Promise拋出的錯誤和rejected狀態),並將錯誤對象作爲then方法rejected狀態下的參數,或者說會被Promise.protorype.catch捕獲到。這裏拋出 的意思是如果錯誤在內部被捕獲處理,則不會停止函數執行。
- 內部的任一rejected狀態的Promise若未在該Promise中被處理或未被try-catch語句捕獲,則也會停止當前函數的執行,會被async返回的Promise對象的catch捕獲到。
await
- await後應該跟一個Promise對象,若不是Promise對象,將會被Promise.resolve()轉化爲Promise對象,並根據是否拋出錯誤而進行不同的操作:
- 若無錯誤拋出則立即成爲resolved狀態,表達式的值被做爲then方法的第一個回調方法的參數。
- 拋出錯誤則成爲rejected狀態並停止函數的執行,錯誤對象做爲catch方法的參數。
防止async函數內部出錯而終止函數剩餘代碼執行的方式
- 對await命令後的Promise對象使用catch方法捕獲錯誤。
- 將這部分代碼放入try-catch語句塊中。
使用技巧
不相關的操作之間不應該互相影響
若一個async函數中的多個await操作之間不存在依賴關係,則不應該讓其中某些await操作等待其他的await操作的完成,應該讓他們併發執行。
async function foo(){
await p1();
await p2();
}
此時p1和p2之間並無任何依賴關係,但是p2只有在p1執行完成以後纔會執行。
//method 1
async function foo(){
await Promise.all(p1(), p2());
}
//method 2
async function foo(){
let a1 = p1();
let a2 = p2();
await a1;
await a2;
}
上述便是可用的兩種讓相互之間無依賴的操作併發執行。
await Promise.all(…)時獲取返回值
async function foo() {
let [a, b] = await Promise.all([p1(), p2()]);
console.log(a, b);
}
實現原理
理解一下阮大佬的代碼我就滿足了[1]:
//genF是generator函數
function spawn(genF) {
//直接返回一個Promise對象
return new Promise(function(resolve, reject) {
//獲取generator的Iterator對象
const gen = genF();
//nextF是回調函數,根據當前Promise狀態做出不同的行爲
function step(nextF) {
//定義next,使其在try-catch語句塊外可見,保存Iterator上調用next的返回值{value: promise, done: boolean}
let next;
try {
//捕獲generator函數可能出現的異常,並使當前Promise對象成爲rejected狀態,將錯誤對象傳遞給當前Promise的catch方法
next = nextF();
} catch(e) {
return reject(e);
}
//判斷generator是否已經執行完成,完成則將返回值傳遞給then方法的第一個回調函數
if(next.done) {
return resolve(next.value);
}
//若未完成,將當前yield後的表達式轉化爲Promise對象,轉換規則見參考1
Promise.resolve(next.value).then(function(v) {
//這裏將resolved狀態下返回的值作爲上一個yield表達式的返回值,並繼續執行下一個yield表達式,這裏的行爲發生在下一輪的step遞歸調用中
step(function() { return gen.next(v); });
}, function(e) {
//過程中的任意錯誤都會停止函數的執行,並拋出異常,注意這裏的使用的是gen.throw(e),直接在generator函數中拋出了異常,而非在自動執行函數中逐層向外拋出。
step(function() { return gen.throw(e); });
});
}
//step函數的首次調用,generator函數執行返回的迭代器首次執行next方法時參數沒有意義,因此此時還沒有上一次執行的yield表達式
step(function() { return gen.next(undefined); });
});
}
async/await函數可以看做是帶自動執行器的generator函數,因此spawn是一個generator函數的自動執行器,注意裏面的return,這裏並不是要將值返回,而是終止當前(層)函數的執行。
雖然現在好像看懂了,估計今晚我就會忘,不要企圖手動執行去驗證超過三層的遞歸函數,否則你會知道什麼叫頭頂發涼,原理嘛理解就行。
tip
forEach的回調函數使用async/await會發生非預期行爲,每次遍歷中的await會併發執行,原因我不太清楚,反正阮一峯大佬是這麼說的,可改爲for循環。
async/await函數暫停執行,即函數會保留當前執行棧
本文中的併發執行,並非是操作系統中通常意義上的線程或進程併發,因爲js執行用戶代碼的線程只有一個,就是主線程,這裏是指多個任務之間的執行互不影響,不用等待其中某些任務狀態發生改變。相互之間執行的時間間隔很短,宏觀上可以看作是單核多線程不考慮同步問題的併發,且每個線程順序執行只一次。
參考
[1] 阮一峯.ES6入門
[2] 湯小丹.計算機操作系統.第4版[M]. 2014.
[3] Promise.all結合async/await