之前寫了篇文章《這一次,徹底理解Promise原理》,剖析了Promise的相關原理。我們都知道,Promise解決了回調地獄的問題,但是如果遇到複雜的業務,代碼裡面會包含大量的 then 函數,使得代碼依然不是太容易閱讀。
基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰,而且還支持 try-catch 來捕獲異常,非常符合人的線性思維。
所以,要研究一下如何實現 async/await。總的來說,async 是Generator函數的語法糖,並對Generator函數進行了改進。
Generator 函數是一個狀態機,封裝了多個內部狀態。執行 Generator 函數會返回一個遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態,但是只有調用next方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield表達式就是暫停標誌。
有這樣一段代碼:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
調用及運行結果:
hw.next()
hw.next()
hw.next()
hw.next()
由結果可以看出,Generator函數被調用時並不會執行,只有當調用next方法、內部指針指向該語句時才會執行,即函數可以暫停,也可以恢復執行。每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。
要搞懂函數為何能暫停和恢復,那你首先要了解協程的概念。
一個線程(或函數)執行到一半,可以暫停執行,將執行權交給另一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種可以並行執行、交換執行權的線程(或函數),就稱為協程。
協程是一種比線程更加輕量級的存在。普通線程是搶先式的,會爭奪cpu資源,而協程是合作的,可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程。它的運行流程大致如下:
協程A開始執行
協程A執行到某個階段,進入暫停,執行權轉移到協程B
協程B執行完成或暫停,將執行權交還A
協程A恢復執行
協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
通常,我們把執行生成器的代碼封裝成一個函數,並把這個執行生成器代碼的函數稱為執行器,co 模塊就是一個著名的執行器。
Generator 是一個異步操作的容器。它的自動執行需要一種機制,當異步操作有了結果,能夠自動交回執行權。兩種方法可以做到這一點:
回調函數。將異步操作包裝成 Thunk 函數,在回調函數裡面交回執行權。
Promise 對象。將異步操作包裝成 Promise 對象,用then方法交回執行權。
一個基於 Promise 對象的簡單自動執行器:
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
我們使用時,可以這樣使用即可,
function* foo() {
let response1 = yield fetch('https://xxx')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://xxx')
console.log('response2')
console.log(response2)
}
run(foo);
上面代碼中,只要 Generator 函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。通過使用生成器配合執行器,就能實現使用同步的方式寫出異步代碼了,這樣也大大加強了代碼的可讀性。
ES7 中引入了 async/await,這種方式能夠徹底告別執行器和生成器,實現更加直觀簡潔的代碼。根據 MDN 定義,async 是一個通過異步執行並隱式返回 Promise 作為結果的函數。可以說async 是Generator函數的語法糖,並對Generator函數進行了改進。
前文中的代碼,用async實現是這樣:
const foo = async () => {
let response1 = await fetch('https://xxx')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://xxx')
console.log('response2')
console.log(response2)
}
一比較就會發現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。
async函數對 Generator 函數的改進,體現在以下四點:
內置執行器。Generator 函數的執行必須依靠執行器,而 async 函數自帶執行器,無需手動執行 next() 方法。
更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裡有異步操作,await表示緊跟在後面的表達式需要等待結果。
更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)。
返回值是 Promise。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then() 方法進行調用。
這裡的重點是自帶了執行器,相當於把我們要額外做的(寫執行器/依賴co模塊)都封裝了在內部。比如:
async function fn(args) {
}
等同於:
function fn(args) {
return spawn(function* () {
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
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); });
});
}
通過上面的分析,我們知道async隱式返回 Promise 作為結果的函數,那麼可以簡單理解為,await後面的函數執行完畢時,await會產生一個微任務(Promise.then是微任務)。但是我們要注意這個微任務產生的時機,它是執行完await之後,直接跳出async函數,執行其他代碼。其他代碼執行完畢後,再回到async函數去執行剩下的代碼,然後把await後面的代碼註冊到微任務隊列當中。我們來看個例子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
分析這段代碼:
執行代碼,輸出script start。
執行async1(),會調用async2(),然後輸出async2 end,此時將會保留async1函數的上下文,然後跳出async1函數。
遇到setTimeout,產生一個宏任務
執行Promise,輸出Promise。遇到then,產生第一個微任務
繼續執行代碼,輸出script end
代碼邏輯執行完畢(當前宏任務執行完畢),開始執行當前宏任務產生的微任務隊列,輸出promise1,該微任務遇到then,產生一個新的微任務
執行產生的微任務,輸出promise2,當前微任務隊列執行完畢。執行權回到async1
執行await,實際上會產生一個promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
執行完成,執行await後面的語句,輸出async1 end