本文是ES2015系列的第七篇文章,上一篇中我们介绍了一些关于iterator的内容。本文讨论一下关于es6中generator的特性以及使用方法和场景。如有书写错误或者逻辑错误的地方,还请多指正。
1.什么是Generator
在传统的函数执行过程中,从函数的开始到结束,往往不会停下来的,比如我们定义了一个函数,这个函数将打印一条信息并返回该信息。在函数执行过程中,我们唯一能控制的就是他的输入内容,函数将执行并退出获得最终的结果。
var fun=function(){
var a='hello'
a=console.log(a+' world');
return a;
}
fun()
如果我们想在函数处理过程中某个点,中断函数执行并传递一个值到a呢? 如果我们想函数奇数次执行的时候只返回a的初始值,偶数次执行才返回最终的值呢?传统的函数实现估计是比较困难的。
Generator的出现使得函数的执行变得更加的灵活,我们可以使用生成器在任意位置暂停执行并传递我们的参数到函数执行位置,赋予函数中变量新的值;还可以控制函数的执行流程和返回的数据。生成器的出现给流程控制增加了新的方式。
2.定义Generator函数
在es6中生成器的定义如下:
function *gen() {
// 这是个Generator函数
}
gen() //执行函数不需要加*
Generator本身在执行的时候比如调用**gen()**的时候,会产生一个迭代器,来控制生成器代码的执行。下面代码中的yield,用来产生一个迭代点,当函数执行到此位置的时候将终止函数执行,等待函数下一次被调用或者直接调用函数产生的迭代器的next()方法,如下面代码。
function * foo(){
var a='hello';
yield a;
a=console.log(a+' world');
return a;
}
var it=foo()
console.log(it.next())
console.log(it.next())
//Output------------------------------------
//{"value":"hello","done":false}
//hello world
//{"done":true}
3. 获取Generator的返回值
对于下面的generator我们如何来获得每一次yield值呢? 基本的方式有两种,一种是使用上面的生成器的方式,另一中就是使用 for of这类可以直接支持迭代器协议的流程控制方法,其实本质上是使用的同样的next()函数依次取值的。
function *foo(){
var arr=[yield 1,yield 2, yield 3]
yield 4
}
3.1. 使用next()方法
使用迭代器方法我们需要手动的调用他的next()方法去执行流程控制,当第一次调用next()的时候暂停在yield 1的位置,并将返回对象中value值赋值为1,由于流程尚未结束,所以done:false ,返回对象为{value:1,done:false}。当流程执行yield4结束后,下一次的next()调用将返回{value:undefined,done:true}
var it=foo()
for(var i=0;;i++){
var result=it.next();
if (result.done){
break;
}
console.log(result)
}
//{ value: 1, done: false }
//{ value: 2, done: false }
//{ value: 3, done: false }
//{ value: 4, done: false }
3.2. 使用for…of循环
使用foo…of循环可以迭代每一次next()函数的返回对象中value的值,当done==='true’时执行结束。测试代码如下:
for(var i of foo()){
console.log(i);
}
//1
//2
//3
//4
4. yield代理
我们可以在一个generator函数中使用yield代理方式,也就是传递另一个可迭代的对象到yield *后面,这样我们可以在当前函数中迭代传递的对象内容。使用上的不同就是需要记住不要忘记yield后面的*。否则生成的就是[1,2,3,4] 将整个的数组一次性yield出来。这里我们还可以传递另一个generator函数进来。
function *foo(){
yield * [1,2,3,4]
}
for(var i of foo()){
console.log(i)
}
//1
//2
//3
//4
5. 传递参数
下面的代码中我们中断函数执行的同时,传递了参数到函数变量中。第一次执行next()的调用将输出一个对象{“value”:“hello”,“done”:false}。第二次执行next(),将从赋值开始执行,传递的’world’将赋值给函数中的a值,所以输出的内容为‘world’,而不再是原来初始化的值。
function *foo(){
var a='hello';
a=yield a;
console.log(a);
}
var it=foo()
console.log(it.next());
it.next('world')
//{"value":"hello","done":false}
//world
6. 中断generator执行
如果我们有一个无限迭代任务的生成器函数,我们想在符合条件的情况下终止他的执行,不再迭代任务出来。可以使用下面的方式:
function *task(){
var i=0;
while(true){yield i++;}
}
var it=task()
console.log(it.next());
console.log(it.next());
it.return(-1); //调用return 函数将终止生成器流程执行
console.log(it.next());
console.log(it.next());
//{"value":0,"done":false}
//{"value":1,"done":false}
//{"done":true}
//{"done":true}
7. 错误处理
下面的代码中我们将模拟一个会产生错误异常的程序,当数据输入过大的时候将报错,在生成器函数中我们依次的输入参数到该test函数中。这时候我们可以使用try…catch来捕获错误信息,流程会终断在出错的位置并将错误信息向上层传递。我们只需要写一个错误处理函数即可。
function test(i){
if(i>10){
throw ('Too big');
}else{
return i
}
}
function *foo(){
for(let i=0;i<100;i++){
yield test(i)
}
}
try{
for(var i of foo()){
console.log(i)
}
}catch(err){
console.log(err)
}
8. 实际使用
8.1 Task.js
Task.js使用Promise和generator来结合完成流程的控制,下面的实例来自于官网,代码中调用task的spawn函数包裹一个生成器函数,该生成器中调用了一些异步的操作,比如ajax,函数将会依次执行,对于异步操作将等待直到获得数据,然后转向下一步操作,这种非callback方式,去除了传统的多层回调的问题,执行的流程更加清晰。
spawn(function*() {
var data = yield $.ajax(url);
$('#result').html(data);
var status = $('#status').html('Download complete.');
yield status.fadeIn().promise();
yield sleep(2000);
status.fadeOut();
});
8.2 co.js
co是tj写的一个流程控制库,该库被用于KOA框架中,作为核心依赖和处理流程使用,他的编写代码的方式如下面所示,co(function *(){}) 也是包裹一个生成器函数,该生成器中,yield对象是一个promise, 当promise执行完毕,流程恢复并传递结果到next()中,执行下一次的迭代。
function getFile(url) {
return fetch(url)
.then(request => request.text());
}
co(function* () {
try {
let [croftStr, bondStr] = yield Promise.all([
getFile('1.json'),
getFile('2.json'),
]);
let croftJson = JSON.parse(croftStr);
let bondJson = JSON.parse(bondStr);
console.log(croftJson);
console.log(bondJson);
} catch (e) {
console.log('Failure to read: ' + e);
}
});
由于co()函数本身返回的是一个promise,我们就可以级联操作,详细的编写操作可查看相关文档。
总结
本文简单的介绍了生成器的使用方法,以及一些例子,对于深入的研究大家可以参考co或者task的源码,co的代码总共就200多行,算是一个典型的现实使用场景了。