ES6必知必会 (七)—— Generator 函数

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 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行,另外,它的函数体内外的数据交换和错误处理机制的特点使它可以作为异步编程的完整解决方案;

推荐阅读更多精彩内容