generator函式與async/await

zyp_beier發表於2021-11-24

理解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命令的語法糖。

相關文章