解決函式回撥經歷了幾個階段, Promise 物件, Generator 函式到async函式。async函式目前是解決函式回撥的最佳方案。很多語言目前都實現了async,包括Python ,java spring,go等。
async await 的用法
async 函式返回一個 Promise 物件,當函式執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再接著執行函式體內後面的語句。
function getNum(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num+1)
}, 1000)
})
}
const func = async ()=>{
const f1 = await getNum(1)
const f2 = await getNum(f1)
console.log(f2)
// 輸出3
}
func()
複製程式碼
async /await 需要在function外部書寫async,在內部需要等待執行的函式前書寫await即可
深入理解
理解async函式需要先理解Generator函式,因為async函式是Generator函式的語法糖
。
Generator[ˈdʒɛnəˌretɚ]函式-生成器
Generator是ES6標準引入的新的資料型別。Generator可以理解為一個狀態機,內部封裝了很多狀態,同時返回一個迭代器Iterator物件。可以通過這個迭代器遍歷相關的值及狀態。 Generator的顯著特點是可以多次返回,每次的返回值作為迭代器的一部分儲存下來,可以被我們顯式呼叫。
Generator函式的宣告
一般的函式使用function宣告,return作為回撥(沒有遇到return,在結尾呼叫return undefined),只可以回撥一次。而Generator函式使用function*定義,除了return,還使用yield返回多次。
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
const result = foo(0) // foo {<suspended>}
result.next(); // {value: 1, done: false}
result.next(); // {value: 2, done: false}
result.next(); // {value: 3, done: true}
result.next(); //{value: undefined, done: true}
複製程式碼
在chrome瀏覽器中這個例子裡,我們可以看到,在執行foo函式後返回了一個
Generator函式的例項。它具有狀態值suspended和closed,suspended代表暫停,closed則為結束。但是這個狀態是無法捕獲的,我們只能通過Generator函式的提供的方法獲取當前的狀態。
在執行next方法後,順序執行了yield的返回值。返回值有value和done兩個狀態。value為返回值,可以是任意型別。done的狀態為false和true,true即為執行完畢。在執行完畢後再次呼叫返回{value: undefined, done: true}
注意:在遇到return的時候,所有剩下的yield不再執行,直接返回{ value: undefined, done: true }
Generator函式的方法
Generator函式提供了3個方法,next/return/throw
next方式是按步執行,每次返回一個值,同時也可以每次傳入新的值作為計算
function* foo(x) {
let a = yield x + 1;
let b= yield a + 2;
return x + 3;
}
const result = foo(0) // foo {<suspended>}
result.next(1); // {value: 1, done: false}
result.next(2); // {value: 2, done: false}
result.next(3); // {value: 3, done: true}
result.next(4); //{value: undefined, done: true}
複製程式碼
return則直接跳過所有步驟,直接返回 {value: undefined, done: true}
throw則根據函式中書寫try catch返回catch中的內容,如果沒有寫try,則直接丟擲異常
function* foo(x) {
try{
yield x+1
yield x+2
yield x+3
yield x+4
}catch(e){
console.log('catch it')
}
}
const result = foo(0) // foo {<suspended>}
result.next(); // {value: 1, done: false}
result.next(); // {value: 2, done: false}
result.throw(); // catch it {value: undefined, done: true}
result.next(); //{value: undefined, done: true}
複製程式碼
這裡可以看到在執行throw之前,順序的執行了狀態,但是在遇到throw的時候,則直接走進catche並改變了狀態。
這裡還要注意一下,因為狀態機是根據執行狀態的步驟而執行,所以如果執行thow的時候,沒有遇到try catch則會直接拋錯 以下面兩個為例
function* foo(x) {
yield x+1
try{
yield x+2
}catch(e){
console.log('catch it')
}
}
const result = foo(0) // foo {<suspended>}
result.next(); // {value: 1, done: false}
result.next(); // {value: 2, done: false}
result.throw(); // catch it {value: undefined, done: true}
result.next(); //{value: undefined, done: true}
複製程式碼
這個例子與之前的執行狀態一樣,因為在執行到throw的時候,已經執行到try語句,所以可以執行,而下面的例子則不一樣
function* foo(x) {
yield x+1
try{
yield x+2
}catch(e){
console.log('catch it')
}
}
const result = foo(0) // foo {<suspended>}
result.next(); // {value: 1, done: false}
result.throw(); // Uncaught undefined
result.next(); //{value: undefined, done: true}
複製程式碼
執行throw的時候,還沒有進入到try語句,所以直接拋錯,丟擲undefined為throw未傳引數,如果傳入引數則顯示為傳入的引數。此狀態與未寫try的拋錯狀態一致。
遍歷
Generator函式的返回值是一個帶有狀態的Generator例項。它可以被for of 呼叫,進行遍歷,且只可被for of 呼叫。此時將返回他的所有狀態
function* foo(x) {
console.log('start')
yield x+1
console.log('state 1')
yield x+2
console.log('end')
}
const result = foo(0) // foo {<suspended>}
for(let i of result){
console.log(i)
}
//start
//1
//state 1
//2
//end
result.next() //{value: undefined, done: true}
複製程式碼
呼叫for of方法後,在後臺呼叫next(),當done屬性為true的時候,迴圈退出。因此Generator函式的例項將順序執行一遍,再次呼叫時,狀態為已完成
狀態的儲存和改變
Generator函式中yield返回的值是可以被變數儲存和改變的。
function* foo(x) {
let a = yield x + 0;
let b= yield a + 2;
yield x;
yield a
yield b
}
const result = foo(0)
result.next() // {value: 0, done: false}
result.next(2) // {value: 4, done: false}
result.next(3) // {value: 0, done: false}
result.next(4) // {value: 2, done: false}
result.next(5) // {value: 3, done: false}
複製程式碼
以上的執行結果中,我們可以看到,在第二步的時候,我們傳入2這個引數,foo函式中的a的變數的值0被替換為2,並且在第4次迭代的時候,返回的是2。而第三次迭代的時候,傳入的3引數,替換了b的值4,並在第5次迭代的時候返回了3。所以傳入的引數,是替代上一次迭代的生成值。
yield* 委託
在Generator函式中,我們有時需要將多個迭代器的值合在一起,我們可以使用yield *的形式,將執行委託給另外一個Generator函式
function* foo1() {
yield 1;
yield 2;
return "foo1 end";
}
function* foo2() {
yield 3;
yield 4;
}
function* foo() {
yield* foo1();
yield* foo2();
yield 5;
}
const result = foo();
console.log(iterator.next());// "{ value: 1, done: false }"
console.log(iterator.next());// "{ value: 2, done: false }"
console.log(iterator.next());// "{ value: 3, done: false }"
console.log(iterator.next());// "{ value: 4, done: false }"
console.log(iterator.next());// "{ value: 5, done: false }"
console.log(iterator.next());// "{ value: undefined, done: true }"
複製程式碼
foo在執行的時候,首先委託給了foo1,等foo1執行完畢,再委託給foo2。但是我們發現,”foo1 end” 這一句並沒有輸出。 在整個Generator中,return只能有一次,在委託的時候,所有的yield*都是以函式表示式的形式出現。return的值是表示式的結果,在委託結束之前其內部都是暫停的,等待到表示式的結果的時候,將結果直接返回給foo。此時foo內部沒有接收的變數,所以未列印。 如果我們希望捕獲這個值,可以使用yield *foo()的方式進行獲取。
實現一個簡單的async/await
如上,我們掌握了Generator函式的使用方法。async/await語法糖就是使用Generator函式+自動執行器來運作的。 我們可以參考以下例子
// 定義了一個promise,用來模擬非同步請求,作用是傳入引數++
function getNum(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num+1)
}, 1000)
})
}
//自動執行器,如果一個Generator函式沒有執行完,則遞迴呼叫
function asyncFun(func){
var gen = func();
function next(data){
var result = gen.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
// 所需要執行的Generator函式,內部的資料在執行完成一步的promise之後,再呼叫下一步
var func = function* (){
var f1 = yield getNum(1);
var f2 = yield getNum(f1);
console.log(f2) ;
};
asyncFun(func);
複製程式碼
在執行的過程中,判斷一個函式的promise是否完成,如果已經完成,將結果傳入下一個函式,繼續重複此步驟。
總結
async/await非常好理解,基本理解了Generator函式之後,幾句話就可以描述清楚。這裡沒有過多的繼續闡述Generator函式的內部執行邏輯及原理,如果有對此有深入理解的童鞋,歡迎補充說明。