天天看點

《十五》ES6中的Generator函數

ES6新引入了Generator函數,可以通過yield關鍵字,把函數的執行流挂起,為改變執行流程提供了可能,進而為異步程式設計提供解決方案。

Generator函數有多種了解角度。文法上,首先可以了解成,Generator函數是一個狀态機,封裝了多個内部狀态;執行Generator函數會傳回一個周遊器對象,也就是說,Generator函數除了是狀态機,還是一個周遊器對象生成函數,傳回的周遊器對象,可以依次周遊Generator函數内部的每一個狀态。形式上,Generator函數是一個普通的函數,但是有兩個特征:function關鍵字和函數名之間有一個星号,星号用來表示函數為Generator函數;函數體内部使用yield語句,yield語句用來定義函數不同的内部狀态。

ES6沒有規定,function關鍵字與函數名之間的星号,寫在哪個位置。下面的寫法都能通過,但一般是第三種。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
           
如果Generator 函數是一個對象的屬性,可以簡寫成下面的形式。
let obj = {
 * myGeneratorMethod() {}
};
           
它的完整形式如下:
let obj = {
 myGeneratorMethod: function* () {}
};
           

Generator函數的調用方法與普通函數一樣,也是在函數名後面加上一對圓括号。不同的是,調用Generator函數後,該函數并不執行,傳回的也不是函數的運作結果,而是一個指向内部狀态的指針對象(是一個周遊器對象)。

下一步,必須調用周遊器對象的next方法,使得指針移向下一個狀态,也就是說,每次調用next方法,内部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式或return表達式。換言之,Generator函數是分段執行的,yield表達式是暫停執行的标記,而next方法可以恢複執行。

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表達式的值hello,done屬性的值false,表示周遊還沒有結束。

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

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

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

總結一下,調用Generator函數,傳回一個周遊器對象,代表Generator函數的内部指針。以後,每次調用周遊器對象的next方法,就會傳回一個有着value和done兩個屬性的對象,value屬性表示目前的内部狀态的值,是yield表達式後面那個表達式的值,done屬性是一個布爾值,表示是否周遊結束。

yield表達式:

由于Generator函數傳回的周遊器對象,隻有調用next方法才會周遊下一個内部狀态,是以其實提供了一種可以暫停執行的函數。yield表達式就是暫停标志。

yield表達式後面的表達式,隻有當調用next方法、内部指針指向該語句時才會執行,是以等于為JavaScript提供了手動的惰性求值的文法功能。

yield*表達式:

如果在Generator函數内部,調用另一個Generator函數,那麼需要在前者的函數内部,自己手動完成周遊。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  // 手動周遊 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

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

上面代碼中,foo和bar都是Generator函數,在bar裡面調用foo,就需要手動周遊foo。如果有多個Generator函數嵌套,寫起來就非常麻煩。

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

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

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

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

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

方法:

  1. next()方法:每次調用next方法,内部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式或者return語句。會傳回一個有着value和done兩個屬性的對象,value屬性表示目前的内部狀态的值,是yield表達式後面那個表達式的值,done屬性是一個布爾值,表示是否周遊結束。
    function* func1(){
          yield '1';
          yield '2';
    }
    let f = func1();
    console.log(f.next());
    console.log(f.next());
    console.log(f.next());
               
    《十五》ES6中的Generator函數
    yield表達式本身是沒有傳回值的,或者說總是傳回undefined。next方法可以帶一個參數,該參數會被當做上一個yield表達式的傳回值。
    function* func1(){
          var x = yield '1';
          console.log(x);
          var y = yield '2';
          console.log(y);
    }
    let f = func1();
    console.log(f.next());
    console.log(f.next());
    console.log(f.next(5));
               
    《十五》ES6中的Generator函數
  2. return()方法:return 方法提供參數時,傳回該參數并結束周遊 Generator 函數;不提供參數時,傳回 undefined并結束周遊 Generator 函數 。
    function* func1(){
            yield '1';
            yield '2';
    }
    let f = func1();
    console.log(f.return());
    console.log(f.return());
               
    《十五》ES6中的Generator函數
    function* func1(){
            yield '1';
            yield '2';
    }
    let f = func1();
    console.log(f.return('哈哈'));
    console.log(f.return());
               
    《十五》ES6中的Generator函數
  3. throw()方法:throw方法可以在Generator函數體外抛出異常,然後在Generator函數體内捕獲。throw()方法可以接受一個參數,這個參數會被catch語句接收。
    function* func1(){
        try{
            yield;
         }catch(e){
            console.log(`Generator函數内部捕獲:${e}`)
         }
    }
    let f = func1();
    f.next();
            
    try{
        f.throw('a');
        f.throw('b');
    }catch(e){
         console.log(`Generator函數外部捕獲:${e}`)
    }
               
    《十五》ES6中的Generator函數
    周遊器對象f連續在體外抛出兩個錯誤。第一個錯誤可以被Generator函數體内的catch語句捕獲;由于Generator函數内部的catch語句已經被執行過了,是以第二個錯誤隻能被函數體外的catch語句捕獲。

使用for…of循環周遊Generator函數:

for…of循環可以自動周遊Generator函數運作時生成的Iterator對象,此時不再需要調用next方法。

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

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

上面代碼使用for…of循環,依次顯示3個yield表達式的值。一旦next方法傳回的對象的done屬性為true,for…of循環就會中止,是以上面代碼的return語句傳回的4,不包括在for…of循環之中。

除了for…of循環以外,擴充運算符、解構指派和Array.from方法内部調用的,都是周遊器接口,這意味着,它們都可以将Generator函數傳回的Iterator對象,作為參數。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 擴充運算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解構指派
let [x, y] = numbers();
x // 1
y // 2

// for...of 循環
for (let n of numbers()) {
  console.log(n)
}// 1、2
           

Generator函數的異步應用:

// 建立axion請求
function query(url) {
    axios.get(url).then(res => {
    	//每次異步任務成功之後,才去執行Generator函數的下一步
         stepsFunc.next(res.data.data)
    });
}

// 建立生成器函數
function* steps() { 
    const sheepNumbers = yield query('http://cg.entrobus.com/api-service/goat/queryAdoptNumber');
    console.log(`第一次接口調用成功:${JSON.stringify(sheepNumbers)}`)
    
    const sheepInfo = yield query(`http://cg.entrobus.com/api-service/goat/queryGoatInfoByNumber?number=${sheepNumbers.numbers[0]}`);
    console.log(`第二次接口調用成功:${JSON.stringify(sheepInfo)}`)
}
const stepsFunc = steps();
stepsFunc.next(); // 流程開啟
           
《十五》ES6中的Generator函數
es6