ES6读书简记·Generator

CY 2019年01月09日 625次浏览

Generator函数看上去好像是一个挺抽象的语法,理解成遍历器生成函数就不那么抽象了。

基本概念

Generator函数会返回一个遍历器对象,除了function后面的*号和函数体内使用的yield,其余和普通函数没有区别。

// 这里的*号在function和函数名的中间,没有位置的限制,但是通常写在function后面
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

hw.next()
// 返回一个对象,value就是当前yield表达式的值,done意思是是否结束了。
// { value: 'hello', done: false }

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

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

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

// done为true后再继续调用next会一直返回 { value: undefined, done: true }
hw.next() 
// { value: undefined, done: true }

和普通函数的调用方法一样,但是调用后并不会执行,返回一个Iterator 对象,需要调用next()方法来移动指针。

一个yield表示一个状态,return也表示一个状态。

每次移动指针都会从上一个yield运行到下一个yield或者return,也可以理解为yield表达式是暂停执行的标记,而next()方法可以恢复执行。

yield和return的区别

yield,函数会暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能,只能执行一次return,但是能执行任意多个yieldyield只能用在Generator函数中,用在其他地方会报错的。

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数,调用next函数才会执行。

yield还可以下面这样用:

// yield表达式如果用在另一个表达式之中,必须放在圆括号里面
console.log('Hello' + (yield 123));
// yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK

因为Generator函数返回一个Iterator,所以可以赋值给对象的Symbol.iterator属性,让对象拥有遍历器。

next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefinednext()方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

next方法第一次调用的时候传递的参数是会被忽略的,没有任何作用的。如果非要在第一次next()中就传递参数,可以将这个Generator函数包装起来,在包装函数里面提前调用一次next()

for...of 循环遍历Generator

for...of遍历的时候不需要手动使用next()函数,而且不会遍历出return的返回值的,也就是不会遍历到donetrue的值。

可以利用这个特性,为原生的对象(不能使用for...of)写一个方法,让其能使用for...of遍历,也可以将Generator函数赋值给对象的Symbol.iterator属性。

除了for...of...运算符,Array.from()都需要这样去遍历。

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw()方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

throw()方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。

如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行

throw()方法抛出的错误要被内部捕获,前提是必须至少执行过一次next()方法。

throw()方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next()方法。

throw命令与g.throw()方法是无关的,两者互不影响。

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next()方法,将返回一个value属性等于undefineddone属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return()方法,可以返回给定的值,并且终结遍历 Generator 函数,相当于value属性变为了return()方法的参数(不提供参数就是undefined),done属性变为了true

如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。

next()、throw()、return() 的共同点

可以理解为:

  • next()是将yield表达式替换成一个值。
  • throw()是将yield表达式替换成一个throw语句。
  • return()是将yield表达式替换成一个return语句。

yield* 表达式

用来在一个 Generator 函数里面执行另一个 Generator 函数。

yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者替代前者。反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员

任何数据结构只要有 Iterator 接口,就可以被yield*遍历

作为对象属性的 Generator 函数

可以简写为下面的形式

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

Generator 函数的this

Generator 函数也不能跟new命令一起用,会报错

以下代码可以让 Generator 函数返回一个正常的对象实例,既可以用next()方法,又可以获得正常的this

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

// 这样就可以使用new了
function F() {
  // 这样就绑定了this了
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

应用

  • 异步操作的同步化表达(改写回调函数)

    可以将回调函数要执行的内容放在yield后面,这样就可以在其他地方通过next的方式来让函数继续执行

  • 部署 Iterator 接口

    可以为任意的对象部署Iterator接口

  • 可以视为数组结构

Generator的异步

原文链接

ES6诞生以前的异步编程解决方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象

有了Generator函数后,可以用yield关键字去分割多个异步操作。通过调用next的方式来继续执行。但是这存在一个问题,Generator需要知道异步执行什么时候执行完成了。

于是可以在Generator函数中使用Promise对象,让其执行next()函数后返回的对象中的value属性为一个Promise,然后执行了next()函数以后需要使用value来判断什么时候异步执行完成了,然后再次调用next()方法,执行剩下的操作。

但是这样带来的问题是,异步代码写的很简洁,但是流程管理很不方便。

Trunk函数

大概的形式是这样的

Trunck(函数)(参数)(回调) // 实现方式查看原文

传入一个函数,将其变为Trunck函数,相当于将参数和回调拆开了。

这样的话写异步函数就可以直接写成下面这样

let returnValue = yield asyncFunctionThunk(参数);

上面的asyncFunctionThunk是经过Trunck转换过的函数,然后在里面传入参数,这样执行next后返回的value是一个可以传入回调的函数,在里面传入回调,就可以知道异步的状态了,然后将回调接收到的值,在传递给下一个next()方法,这样就可以让returnValue接收到值了。

大概写成下面这样:

g.next().value(function (err, data) {
  if (err) throw err;
  g.next(data).value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

这样就可以按照上面的代码,写成递归的形式来自动执行Generator中的异步函数了。

除了Trunk,还有其他的方式,例如:co模块

用法大概是下面的样子

var co = require('co');
co(gen);

使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象,或者是Promise 对象数组

同样也是利用递归的方式来自动执行Generator函数。

Generator和Stream