async/await 原理及簡單實現

萌娃奶爸發表於2019-05-08

解決函式回撥經歷了幾個階段, 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函式的內部執行邏輯及原理,如果有對此有深入理解的童鞋,歡迎補充說明。

相關文章