1. 程式人生 > >ES6必知必會 (七)—— Generator 函數

ES6必知必會 (七)—— Generator 函數

獲取 out through 屬性表 rom 代碼 指針 task 不執行

Generator 函數

1.Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同,通常有兩個特征:

  • function關鍵字與函數名之間有一個星號;

  • 函數體內部使用yield表達式,定義不同的內部狀態
    //一個簡單的 Generator 函數
    function *Generator(){
    yield ‘Hello‘;
    yield ‘World‘;

     return ‘Hello World‘;
    }
    

2.Generator 函數的調用方法與普通函數一樣,也是在函數名後面加上一對圓括號。不同的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,必須調用 next 方法,才能使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)為止。實際上就是,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法可以恢復執行。

function *Generator(){
    yield ‘Hello‘;
    yield ‘World‘;

    return ‘Hello World‘;
}

let generator = Generator();  //無返回

generator.next();  
//{"value":"Hello","done":false}

generator.next();
//{"value":"World","done":false}

generator.next();
//{"value":"Hello World","done":true}

generator.next();
//{"value":undefined , "done":true}

上述代碼就是一個 Generator 函數的執行過程 :

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

3.上述例子中我們可以得知,調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以後,每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式(或return)後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

4.yield 表達式可以看做是Generator 函數的暫停標誌,要註意的是 yield 表達式後面的表達式,只有當調用next方法、內部指針指向該語句時才會執行;

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

上面代碼中,yield後面的表達式123 + 456,不會立即求值,只會在next方法將指針移到這一句時,才會求值(“惰性求值”);

5.yield 表達式與 return 語句都能返回緊跟在語句後面的那個表達式的值,不同的是遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而 return 語句不具備位置記憶的功能,而且一個函數裏面,只能執行一個 return 語句,但是可以執行多個yield表達式,要註意的是 yield 表達式只能用在 Generator 函數裏面,用在其他地方都會報錯。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

6.Generator 函數可以不用yield表達式,這時就變成了一個單純的暫緩執行函數

function *Generator() {
  console.log(‘Hello World!‘)
}

var generator = Generator();

setTimeout(function () {
  generator.next()
}, 2000);

// "Hello World"

7.yield 表達式如果用在另一個表達式之中,必須放在圓括號裏面,如果用作函數參數或放在賦值表達式的右邊,可以不加括號

function *Generator() {
  console.log(‘Hello‘ + yield);         // SyntaxError
  console.log(‘Hello‘ + yield 123);     // SyntaxError

  console.log(‘Hello‘ + (yield));       // OK
  console.log(‘Hello‘ + (yield 123));   // OK
}

function *Generator() {
  foo( yield ‘a‘ , yield ‘b‘ );         // OK
  let input = yield;                    // OK
}

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

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

var g = Generator();

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

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

我們利用這個特性,在 Generator 函數運行的不同階段,從外部向內部註入不同的值,從而調整函數行為;

function *foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next()            // {value:6, done:false}
a.next()            // {value:NaN, done:false}
a.next()            // {value:NaN, done:true}

var b = foo(5);
b.next()            // { value:6, done:false }
b.next(12)          // { value:8, done:false }
b.next(13)          // { value:42, done:true }

上述代碼 a 第二次運行 next 方法的時候不帶參數,導致 y 的值等於2 * undefined(即NaN),除以3以後還是NaN,因此返回對象的value屬性也等於NaN。第三次運行Next方法的時候不帶參數,所以z等於undefined,返回對象的value屬性等於5 + NaN + undefined,即NaN;

b 調用加了參數,結果就不一樣了,第一次調用next方法時,返回 x+1 的值 6;第二次調用next方法,將上一次 yield 表達式的值設為 12 ,因此 y 等於 24,返回 y / 3 的值 8;第三次調用next方法,將上一次 yield 表達式的值設為 13 ,因此 z 等於 13 ,這時 x 等於 5,y 等於24,所以 return 語句的值等於 42;

ps:next 方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的

9.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 方法了,但是要註意的是,一旦 next 方法的返回對象的 done 屬性為 true時,for...of循環就會中止,且不包含該返回對象,因此上面 return 語句返回的 6,不包括在for...of循環之中。

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

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

var g = Generator();

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

遍歷器對象 g 調用 return 方法後,返回值的 value 屬性就是 return 方法的參數 ok(如果 不提供參數,則返回值的 value 屬性為 undefined) 。並且,Generator函數的遍歷就終止了,返回值的 done 屬性為 true,以後再調用 next 方法,value 總是返回 undefined , done 屬性總是返回 true;

11.如果在 Generator 函數內部,調用另一個 Generator 函數,默認情況下是沒有效果的。

function *Generator1() {
  yield ‘a‘;
  yield ‘b‘;
}

function *Generator2() {
  yield ‘x‘;
  Generator1();
  yield ‘y‘;
}

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

12.可以使用 yield* 表達式,用來在一個 Generator 函數裏面執行另一個 Generator 函數來達到在 Generator 函數內部,調用另一個 Generator 函數的效果;

function *Generator1() {
  yield ‘a‘;
  yield ‘b‘;
}

function *Generator2() {
  yield ‘x‘;
  yield* Generator1();
  yield ‘y‘;
}

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

// 等同於
function *Generator2() {
  yield ‘x‘;
  for (let v of Generator2()) {
    yield v;
  }
    yield ‘y‘;
}

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

13.Generator 可以暫停函數執行,返回任意表達式的值。這種特點使得 Generator 有多種應用場景;

  • 異步操作的同步化表達 , 可以利用 Generator 函數的暫停執行的效果,把異步操作寫在 yield 表達式裏面,等到調用next方法時再往後執行
    function *loading() {
    showLoadingScreen();
    yield loadDataAsync();
    hideLoadingScreen();
    }
    var loader = loading();
    // 加載loading
    loader.next()

     // 隱藏loading
     loader.next()
    

上面代碼中,第一次調用 loading 函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面(showLoadingScreen),並且異步加載數據(loadDataAsync)。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面,整個邏輯就很清晰了~

  • 可以利用 Generator 函數用同步的方式部署 Ajax 操作

     function *main(url) {
       var result = yield request( url );
       var resp = JSON.parse(result);
       console.log(resp.value);
     }
    
     function request(url) {
       makeAjaxCall(url, function(response){
      it.next(response);
       });
     }
    
     var ajax = main();
     ajax.next();
    

上面代碼的 main 函數,就是通過 Ajax 操作獲取數據,要註意的是 makeAjaxCall 函數中的 next 方法,必須加上 response 參數,因為 yield 表達式,本身是沒有值的,總是等於undefined;

  • 控制流管理,如果有一個多步操作非常耗時,采用回調函數,可能會寫成下面這樣:

     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
      })

采用 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
      }
    }

然後使用一個自動化函數,按次序執行所有步驟

    scheduler(longRunningTask(initialValue));

    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函數未結束,就繼續調用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }

14.另外,Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權(即暫停執行),遇到 yield 命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行,另外,它的函數體內外的數據交換和錯誤處理機制的特點使它可以作為異步編程的完整解決方案;




ES6必知必會 (七)—— Generator 函數