generator函數與async/await

理解async函數就要先理解generator函數,因為async就是Generator函數的語法糖

Generator 函數

Generator 函數是 ES6 提供的一種非同步編程解決方案,可以先理解為一個狀態機,封裝了多個內部狀態,執行Generator函數返回一個遍歷器對象,通過遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態
語法上,Generator 函數是一個普通函數,但是有兩個特徵。
一是,function關鍵字與函數名之間有一個星號;
二是,函數體內部使用yield表達式,定義不同的內部狀態(yield在英語里的意思就是「產出」);

function* helloGenerator() {
  yield 'hello'
  yield 'Generator'
  return 'ending'
}

let Generator = helloGenerator()

調用Generator函數後並不執行,返回的也不是函數運行結果而是一個指向內部狀態的指針對象,也就是遍歷器對象(Iterator Object)。
必須調用遍歷器對象的next方法,使得指針移向下一個狀態。

console.log(Generator.next())  //  {value: 'hello', done: false}
console.log(Generator.next())  //  {value: 'Generator', done: false}
console.log(Generator.next())  //  {value: 'ending', done: true}

第一次調用next方法,Generator函數開始執行,直到遇到yield表達式為止。next方法返回一個對象,value屬性就是當前yield表達式的值hello,done屬性的值false,表示遍歷還沒有結束。
第二次調用next方法,Generator 函數從上次yield表達式停下的地方,一直執行到下一個yield表達式
繼續調用next方法直到done屬性值為true或者執行到return語句(如果沒有return語句就執行到函數結束),表示遍歷已經結束
如果再次調用next方法,此時Generator函數已經運行完畢,next方法返回對象的value屬性為undefined,done屬性為true。以後再調用next方法,返回的都是這個值

yield 表達式

可以理解為暫停的標誌,遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield後面的那個表達式的值,作為返回的對象的value屬性值。yield表達式與return語句都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。一個函數裡面,只能執行一次return語句,但是可以執行多次yield表達式。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是「生成器」的意思)。另外需要注意,yield表達式只能用在 Generator 函數裡面,用在其他地方都會報錯。

next方法的參數

next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值

function* foo(x) {
  let y = yield  x + 1
  let k = yield y + 2
  yield k / 2
  return k
}

let a = foo(1)

console.log(a.next())   //  {value: 2, done: false}
console.log(a.next(3))  //  {value: 5, done: false}
console.log(a.next(8))  //  {value: 4, done: false}
console.log(a.next())   //  {value: 8, done: true}

第一次運行next方法時,返回1+1的值2;第二次調用next方法,將上一次yield表達式的值設為3,y等於3,返回y + 2的值5;第三次調用next方法,將上一次yield表達式的值設為8,k等於8,返回k/2的值4
注意,由於next方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的。

next()、throw()、return()

除了next方法還有throw()、return()兩個方法,這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函數恢復執行,並且使用不同的語句替換yield表達式。
next()是將yield表達式替換成一個值。
throw()是將yield表達式替換成一個throw語句。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當於將 let result = yield x + y 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句。

gen.return(2); // {value: 2, done: true}
// 相當於將 let result = yield x + y替換成 let result = return 2;

yield* 表達式

ES6提供了yield*表達式,用來在一個Generator函數裡面執行另一個Generator函數。

function* foo(x) {
  
  yield 1
  yield* bar()
  yield 4
}

function* bar() {
  yield 2
  yield 3
}

let a = foo()

console.log(a.next())   //  {value: 1, done: false}
console.log(a.next())   //  {value: 2, done: false}
console.log(a.next())   //  {value: 3, done: false}
console.log(a.next())   //  {value: 4, done: false}
console.log(a.next())   //  {value: undefined, done: true}

由於yield* bar()語句得到的值,是一個遍歷器,所以要用星號表示。運行結果就是使用一個遍歷器,遍歷了多個Generator函數,有遞歸的效果。
yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for…of循環。

async/await

ES7 中引入了 async/await,async 是一個通過非同步執行並隱式返回 Promise 作為結果的函數。async 函數的實現原理,就是將 Generator函數和自動執行器,包裝在一個函數里。

根據阮一峰老師的介紹,async函數就是Generator函數的語法糖,並對Generator函數進行了改進。

上面程式碼async函數就是將Generator函數的星號(*)替換成async,將yield替換成await,僅此而已
async函數對 Generator 函數的改進,體現在以下四點

  1. 內置執行器
    Generator 函數的執行必須靠執行器,需要調用next方法,才能真正執行,得到最後結果。
  2. 更好的語義
    async和await,比起星號和yield,語義更加清楚。async表示函數里有非同步操作,await表示緊跟在後面的表達式需要等待結果。
  3. 更廣的適用性
    co模組約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,可以是 Promise 對象和原始類型的值(數值、字元串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)
  4. 返回值是promise
    async函數的返回值是Promise對象,比Generator函數的返回值是Iterator對象方便多了。可以用then方法指定下一步的操作。
    async函數完全可以看作多個非同步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
Tags: