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
,但是能执行任意多个yield
,yield
只能用在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
表达式本身没有返回值,或者说总是返回undefined
。next()
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
next
方法第一次调用的时候传递的参数是会被忽略的,没有任何作用的。如果非要在第一次next()
中就传递参数,可以将这个Generator
函数包装起来,在包装函数里面提前调用一次next()
。
for...of 循环遍历Generator
for...of
遍历的时候不需要手动使用next()
函数,而且不会遍历出return
的返回值的,也就是不会遍历到done
为true
的值。
可以利用这个特性,为原生的对象(不能使用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
属性等于undefined
、done
属性等于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
函数。