ES6 Generator 函數

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同

Generator 函數有多種理解角度。語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。

執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。

形式上,Generator 函數是一個普通函數,但是有兩個特徵。一是,function關鍵字與函數名之間有一個*;二是,函數體內部使用yield表達式,定義不同的內部狀態(yield在英語裏的意思就是“產出”)

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代碼一共調用了四次next方法。

第一次調用,Generator 函數開始執行,直到遇到第一個yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hellodone屬性的值false,表示遍歷還沒有結束

第二次調用,Generator函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值worlddone屬性的值false,表示遍歷還沒有結束

第三次調用,Generator函數從上次yield表達式停下的地方,一直執行到return語句(如果沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(如果沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。

第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefined,done屬性爲true。以後再調用next方法,返回的都是這個值。

ES6 沒有規定,function關鍵字與函數名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

由於 Generator 函數仍然是普通函數,所以一般的寫法是上面的第三種,即星號緊跟在function關鍵字後面。本書也採用這種寫法。

yield表達式

由於 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield表達式就是暫停標誌。

遍歷器對象的next方法的運行邏輯如下

  • 1、遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield後面的那個表達式的值,作爲返回的對象的value屬性值。
  • 2、下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
  • 3、如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到`return語句爲止,並將return語句後面的表達式的值,作爲返回的對象的value屬性值。
  • 4、如果該函數沒有return語句,則返回的對象的value屬性值爲undefined

需要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,因此等於爲 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;
}

上面代碼中,yield後面的表達式123 + 456,不會立即求值,只會在next方法將指針移到這一句時,纔會求值。

next方法的參數

yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

上面代碼先定義了一個可以無限運行的 Generator 函數f,如果next方法沒有參數,每次運行到yield表達式,變量reset的值總是undefined。當next方法帶一個參數true時,變量reset就被重置爲這個參數(即true),因此i會等於-1,下一輪循環就會從-1開始遞增。

這個功能有很重要的語法意義。Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。通過next方法的參數,就有辦法在 Generator 函數開始運行之後,繼續向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行爲。

for...of循環

for...of循環可以自動遍歷 Generator 函數運行時生成的Iterator對象,且此時不再需要調用next方法

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

上面代碼使用for...of循環,依次顯示 5 個yield表達式的值。這裏需要注意,一旦next方法的返回對象的done屬性爲true,for...of循環就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of循環之中

** return**

Generator 函數返回的遍歷器對象,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函數。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

上面代碼中,遍歷器對象g調用return方法後,返回值的value屬性就是return方法的參數foo。並且,Generator 函數的遍歷就終止了,返回值的done屬性爲true,以後再調用next方法,done屬性總是返回true。

如果return方法調用時,不提供參數,則返回值的value屬性爲undefined

yield 表達式*

如果在 Generator 函數內部,調用另一個 Generator 函數,默認情況下是沒有效果的,這個就需要用到yield*表達式,用來在一個 Generator 函數裏面執行另一個

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

Generator 函數簡單應用

(1)異步操作的同步化表達

Generator 函數的暫停執行的效果,意味着可以把異步操作寫在yield表達式裏面,等到調用next方法時再往後執行。這實際上等同於不需要寫回調函數了,因爲異步操作的後續操作可以放在yield表達式下面,反正要等到調用next方法時再執行。所以,Generator 函數的一個重要實際意義就是用來處理異步操作,改寫回調函數。

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()

// 卸載UI
loader.next()

上面代碼中,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面(showLoadingScreen),並且異步加載數據(loadUIDataAsynchronously)。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面。可以看到,這種寫法的好處是所有Loading界面的邏輯,都被封裝在一個函數,按部就班非常清晰。

(2)控制流管理

如果有一個多步操作非常耗時,採用回調函數,可能會寫成下面這樣。

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

採用 Promise 改寫上面的代碼。

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

上面代碼已經把回調函數,改成了直線執行的形式,但是加入了大量 Promise 的語法。Generator 函數可以進一步改善代碼運行流程。

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章