1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-25 19:22:21 +00:00
es6tutorial/docs/generator.md
2015-07-12 20:08:11 +08:00

1050 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Generator 函数
## 简介
### 基本概念
Generator函数是ES6提供的一种异步编程解决方案语法行为与传统函数完全不同。本章详细介绍Generator函数的语法和API它的异步编程应用请看《异步操作》一章。
Generator函数有多种理解角度。从语法上首先可以把它理解成一个函数的内部状态的遍历器也就是说Generator函数是一个状态机。它每调用一次就进入下一个内部状态。Generator函数可以控制内部状态的变化依次遍历这些状态。
形式上Generator函数是一个普通函数但是有两个特征。一是function命令与函数名之间有一个星号二是函数体内部使用yield语句定义遍历器的每个成员即不同的内部状态yield语句在英语里的意思就是“产出”
```javascript
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
```
上面代码定义了一个Generator函数helloWorldGenerator它内部有两个yield语句“hello”和“world”即该函数有三个状态helloworld和return语句结束执行
然后Generator函数的调用方法与普通函数一样也是在函数名后面加上一对圆括号。不同的是调用Generator函数后该函数并不执行返回的也不是函数运行结果而是一个指向内部状态的指针对象也就是上一章介绍的遍历器对象Iterator Object
下一步必须调用遍历器对象的next方法使得指针移向下一个状态。也就是说每次调用next方法内部指针就从函数头部或上一次停下来的地方开始执行直到遇到下一个yield语句或return语句为止。换言之Generator函数是分段执行的yield命令是暂停执行的标记而next方法可以恢复执行。
```javascript
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语句的值hellodone属性的值false表示遍历还没有结束。
第二次调用Generator函数从上次yield语句停下的地方一直执行到下一个yield语句。next方法返回的对象的value属性就是当前yield语句的值worlddone属性的值false表示遍历还没有结束。
第三次调用Generator函数从上次yield语句停下的地方一直执行到return语句如果没有return语句就执行到函数结束。next方法返回的对象的value属性就是紧跟在return语句后面的表达式的值如果没有return语句则value属性的值为undefineddone属性的值true表示遍历已经结束。
第四次调用此时Generator函数已经运行完毕next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法返回的都是这个值。
总结一下调用Generator函数返回一个部署了Iterator接口的遍历器对象用来操作内部指针。以后每次调用遍历器对象的next方法就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值是yield语句后面那个表达式的值done属性是一个布尔值表示是否遍历结束。
### yield语句
由于Generator函数返回的遍历器只有调用next方法才会遍历下一个内部状态所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。
遍历器next方法的运行逻辑如下。
1遇到yield语句就暂停执行后面的操作并将紧跟在yield后面的那个表达式的值作为返回的对象的value属性值。
2下一次调用next方法时再继续往下执行直到遇到下一个yield语句。
3如果没有再遇到新的yield语句就一直运行到函数结束直到return语句为止并将return语句后面的表达式的值作为返回的对象的value属性值。
4如果该函数没有return语句则返回的对象的value属性值为undefined。
需要注意的是yield语句后面的表达式只有当调用next方法、内部指针指向该语句时才会执行因此等于为JavaScript提供了手动的“惰性求值”Lazy Evaluation的语法功能。
```javascript
function* gen{
yield 123 + 456;
}
```
上面代码中yield后面的表达式`123 + 456`不会立即求值只会在next方法将指针移到这一句时才会求值。
yield语句与return语句既有相似之处也有区别。相似之处在于都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield函数暂停执行下一次再从该位置继续向后执行而return语句不具备位置记忆的功能。一个函数里面只能执行一次或者说一个return语句但是可以执行多次或者说多个yield语句。正常函数只能返回一个值因为只能执行一次returnGenerator函数可以返回一系列的值因为可以有任意多个yield。从另一个角度看也可以说Generator生成了一系列的值这也就是它的名称的来历在英语中generator这个词是“生成器”的意思
Generator函数可以不用yield语句这时就变成了一个单纯的暂缓执行函数。
```javascript
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
```
上面代码中函数f如果是普通函数在为变量generator赋值时就会执行。但是函数f是一个Generator函数就变成只有调用next方法时函数f才会执行。
另外需要注意yield语句不能用在普通函数中否则会报错。
```javascript
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
```
上面代码在一个普通函数中使用yield语句结果产生一个句法错误。
下面是另外一个例子。
```javascript
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a){
a.forEach(function(item){
if (typeof item !== 'number'){
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)){
console.log(f);
}
```
上面代码也会产生句法错误因为forEach方法的参数是一个普通函数但是在里面使用了yield语句。一种修改方法是改用for循环。
```javascript
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a){
var length = a.length;
for(var i =0;i<length;i++){
var item = a[i];
if (typeof item !== 'number'){
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)){
console.log(f);
}
// 1, 2, 3, 4, 5, 6
```
### 与Iterator的关系
上一章说过任意一个对象的Symbol.iterator属性等于该对象的遍历器函数调用该函数会返回该对象的一个遍历器。
遍历器本身也是一个对象它的Symbol.iterator属性执行后返回自身。
```javascript
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
```
上面代码中gen是一个Generator函数调用它会生成一个遍历器g。遍历器g的Symbol.iterator属性是一个遍历器函数执行后返回它自己。
## next方法的参数
yield语句本身没有返回值或者说总是返回undefined。next方法可以带一个参数该参数就会被当作上一个yield语句的返回值。
```javascript
function* f() {
for(var i=0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
```
上面代码先定义了一个可以无限运行的Generator函数f如果next方法没有参数每次运行到yield语句变量reset的值总是undefined。当next方法带一个参数true时当前的变量reset就被重置为这个参数即true因此i会等于-1下一轮循环就会从-1开始递增。
这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行它的上下文状态context是不变的。通过next方法的参数就有办法在Generator函数开始运行之后继续向函数体内部注入值。也就是说可以在Generator函数运行的不同阶段从外部向内部注入不同的值从而调整函数行为。
再看一个例子。
```javascript
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:false}
```
上面代码中第二次运行next方法的时候不带参数导致y的值等于`2 * undefined`即NaN除以3以后还是NaN因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数所以z等于undefined返回对象的value属性等于`5 + NaN + undefined`即NaN。
如果向next方法提供参数返回结果就完全不一样了。
```javascript
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var it = foo(5);
it.next()
// { value:6, done:false }
it.next(12)
// { value:8, done:false }
it.next(13)
// { value:42, done:true }
```
上面代码第一次调用next方法时返回`x+1`的值6第二次调用next方法将上一次yield语句的值设为12因此y等于24返回`y / 3`的值8第三次调用next方法将上一次yield语句的值设为13因此z等于13这时x等于5y等于24所以return语句的值等于42。
注意由于next方法的参数表示上一个yield语句的返回值所以第一次使用next方法时不能带有参数。V8引擎直接忽略第一次使用next方法时的参数只有从第二次使用next方法开始参数才是有效的。
## for...of循环
for...of循环可以自动遍历Generator函数且此时不再需要调用next方法。
```javascript
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方法的返回对象的done属性为truefor...of循环就会中止且不包含该返回对象所以上面代码的return语句返回的6不包括在for...of循环之中。
下面是一个利用generator函数和for...of循环实现斐波那契数列的例子。
```javascript
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
```
从上面代码可见使用for...of语句时不需要使用next方法。
## throw方法
Generator函数还有一个特点它可以在函数体外抛出错误然后在函数体内捕获。
```javascript
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
```
上面代码中遍历器i连续抛出两个错误。第一个错误被Generator函数体内的catch捕获然后Generator函数执行完成于是第二个错误被函数体外的catch捕获。
注意上面代码的错误是用遍历器的throw方法抛出的而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。
```javascript
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
```
上面代码之所以只捕获了a是因为函数体外的catch语句块捕获了抛出的a错误以后就不会再继续执行try语句块了。
如果遍历器函数内部没有部署try...catch代码块那么throw方法抛出的错误将被外部try...catch代码块捕获。
```javascript
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
```
上面代码中遍历器函数g内部没有部署try...catch代码块所以抛出的错误直接被外部catch代码块捕获。
如果遍历器函数内部部署了try...catch代码块那么遍历器的throw方法抛出的错误不影响下一次遍历否则遍历直接终止。
```javascript
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
g.throw();
} catch (e) {
g.next();
}
// hello
```
上面代码只输出hello就结束了因为第二次调用next方法时遍历器状态已经变成终止了。但是如果使用throw方法抛出错误不会影响遍历器状态。
```javascript
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
```
上面代码中throw命令抛出的错误不会影响到遍历器的状态所以两次执行next方法都取到了正确的操作。
这种函数体内捕获错误的机制,大大方便了对错误的处理。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数写一个错误处理语句。
```javascript
foo('a', function (a) {
if (a.error) {
throw new Error(a.error);
}
foo('b', function (b) {
if (b.error) {
throw new Error(b.error);
}
foo('c', function (c) {
if (c.error) {
throw new Error(c.error);
}
console.log(a, b, c);
});
});
});
```
使用Generator函数可以大大简化上面的代码。
```javascript
function* g(){
try {
var a = yield foo('a');
var b = yield foo('b');
var c = yield foo('c');
} catch (e) {
console.log(e);
}
console.log(a, b, c);
}
```
反过来Generator函数内抛出的错误也可以被函数体外的catch捕获。
```javascript
function *foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
```
上面代码中第二个next方法向函数体内传入一个参数42数值是没有toUpperCase方法的所以会抛出一个TypeError错误被函数体外的catch捕获。
一旦Generator执行过程中抛出错误就不会再执行下去了。如果此后还调用next方法将返回一个value属性等于undefined、done属性等于true的对象即JavaScript引擎认为这个Generator已经运行结束了。
```javascript
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
```
上面代码一共三次运行next方法第二次运行的时候会抛出错误然后第三次运行的时候Generator函数就已经结束了不再执行下去了。
## yield*语句
如果yield命令后面跟的是一个遍历器需要在yield命令后面加上星号表明它返回的是一个遍历器。这被称为yield*语句。
```javascript
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
```
上面代码中delegatingIterator是代理者delegatedIterator是被代理者。由于`yield* delegatedIterator`语句得到的值是一个遍历器所以要用星号表示。运行结果就是使用一个遍历器遍历了多个Genertor函数有递归的效果。
yield*语句等同于在Generator函数内部部署一个for...of循环。
```javascript
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
```
上面代码说明yield*不过是for...of的一种简写形式完全可以用后者替代前者。
再来看一个对比的例子。
```javascript
function* inner() {
yield 'hello!'
}
function* outer1() {
yield 'open'
yield inner()
yield 'close'
}
var gen = outer1()
gen.next() // -> 'open'
gen.next() // -> a generator
gen.next() // -> 'close'
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next() // -> 'open'
gen.next() // -> 'hello!'
gen.next() // -> 'close'
```
上面例子中outer2使用了`yield*`outer1没使用。结果就是outer1返回一个遍历器outer2返回该遍历器的内部值。
如果`yield*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
```javascript
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
```
上面代码中yield命令后面如果不加星号返回的是整个数组加了星号就表示返回的是数组的遍历器。
如果被代理的Generator函数有return语句那么就可以向代理它的Generator函数返回数据。
```javascript
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next(); //
it.next(); //
it.next(); //
it.next(); // "v: foo"
it.next(); //
```
上面代码在第四次调用next方法的时候屏幕上会有输出这是因为函数foo的return语句向函数bar提供了返回值。
`yield*`命令可以很方便地取出嵌套数组的所有成员。
```javascript
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
```
下面是一个稍微复杂的例子使用yield*语句遍历完全二叉树。
```javascript
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序inorder遍历函数。
// 由于返回的是一个遍历器所以要用generator函数。
// 函数体内采用递归算法所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
```
## 作为对象属性的Generator函数
如果一个对象的属性是Generator函数可以简写成下面的形式。
```javascript
let obj = {
* myGeneratorMethod() {
···
}
};
```
上面代码中myGeneratorMethod属性前面有一个星号表示这个属性是一个Generator函数。
它的完整形式如下,与上面的写法是等价的。
```javascript
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
```
## Generator函数推导
ES7在数组推导的基础上提出了Generator函数推导Generator comprehension
```javascript
let generator = function* () {
for (let i = 0; i < 6; i++) {
yield i;
}
}
let squared = ( for (n of generator()) n * n );
// 等同于
// let squared = Array.from(generator()).map(n => n * n);
console.log(...squared);
// 0 1 4 9 16 25
```
“推导”这种语法结构不仅可以用于数组ES7将其推广到了Generator函数。for...of循环会自动调用遍历器的next方法将返回值的value属性作为数组的一个成员。
Generator函数推导是对数组结构的一种模拟它的最大优点是惰性求值即直到真正用到时才会求值这样可以保证效率。请看下面的例子。
```javascript
let bigArray = new Array(100000);
for (let i = 0; i < 100000; i++) {
bigArray[i] = i;
}
let first = bigArray.map(n => n * n)[0];
console.log(first);
```
上面例子遍历一个大数组但是在真正遍历之前这个数组已经生成了占用了系统资源。如果改用Generator函数推导就能避免这一点。下面代码只在用到时才会生成一个大数组。
```javascript
let bigGenerator = function* () {
for (let i = 0; i < 100000; i++) {
yield i;
}
}
let squared = ( for (n of bigGenerator()) n * n );
console.log(squared.next());
```
## 含义
### Generator与状态机
Generator是实现状态机的最佳结构。比如下面的clock函数就是一个状态机。
```javascript
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
```
上面代码的clock函数一共有两种状态Tick和Tock每运行一次就改变一次状态。这个函数如果用Generator实现就是下面这样。
```javascript
var clock = function*(_) {
while (true) {
yield _;
console.log('Tick!');
yield _;
console.log('Tock!');
}
};
```
上面的Generator实现与ES5实现对比可以看到少了用来保存状态的外部变量ticking这样就更简洁更安全状态不会被非法篡改、更符合函数式编程的思想在写法上也更优雅。Generator之所以可以不用外部变量保存状态是因为它本身就包含了一个状态信息即目前是否处于暂停态。
### Generator与协程
协程coroutine是一种程序运行的方式可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现也可以用多线程实现。前者是一种特殊的子例程后者是一种特殊的线程。
**1协程与子例程的差异**
传统的“子例程”subroutine采用堆栈式“后进先出”的执行方式只有当调用的子函数完全执行完毕才会结束执行父函数。协程与其不同多个线程单线程情况下即多个函数可以并行执行但是只有一个线程或函数处于正在运行的状态其他线程或函数都处于暂停态suspended线程或函数之间可以交换执行权。也就是说一个线程或函数执行到一半可以暂停执行将执行权交给另一个线程或函数等到稍后收回执行权的时候再恢复执行。这种可以并行执行、交换执行权的线程或函数就称为协程。
从实现上看在内存中子例程只使用一个栈stack而协程是同时存在多个栈但只有一个栈是在运行状态也就是说协程是以多占用内存为代价实现多任务的并行。
**2协程与普通线程的差异**
不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于ECMAScript是单线程语言只能保持一个调用栈。引入协程以后每个任务可以保持自己的调用栈。这样做的最大好处就是抛出错误的时候可以找到原始的调用栈。不至于像异步操作的回调函数那样一旦出错原始的调用栈早就结束。
Generator函数是ECMAScript 6对协程的实现但属于不完全实现。Generator函数被称为“半协程”semi-coroutine意思是只有Generator函数的调用者才能将程序的执行权还给Generator函数。如果是完全执行的协程任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程完全可以将多个需要互相协作的任务写成Generator函数它们之间使用yield语句交换控制权。
## 应用
Generator可以暂停函数执行返回任意表达式的值。这种特点使得Generator有多种应用场景。
### 1异步操作的同步化表达
Generator函数的暂停执行的效果意味着可以把异步操作写在yield语句里面等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了因为异步操作的后续操作可以放在yield语句下面反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用来处理异步操作改写回调函数。
```javascript
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
```
上面代码表示第一次调用loadUI函数时该函数不会执行仅返回一个遍历器。下一次对该遍历器调用next方法则会显示Loading界面并且异步加载数据。等到数据加载完成再一次使用next方法则会隐藏Loading界面。可以看到这种写法的好处是所有Loading界面的逻辑都被封装在一个函数按部就班非常清晰。
Ajax是典型的异步操作通过Generator函数部署Ajax操作可以用同步的方式表达。
```javascript
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
```
上面代码的main函数就是通过Ajax操作获取数据。可以看到除了多了一个yield它几乎与同步操作的写法完全一样。注意makeAjaxCall函数中的next方法必须加上response参数因为yield语句构成的表达式本身是没有值的总是等于undefined。
下面是另一个例子通过Generator函数逐行读取文本文件。
```javascript
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
```
上面代码打开文本文件使用yield语句可以手动逐行读取文件。
### 2控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
```javascript
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
```
采用Promise改写上面的代码。
```javascript
Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
```
上面代码已经把回调函数改成了直线执行的形式但是加入了大量Promise的语法。Generator函数可以进一步改善代码运行流程。
```javascript
function* longRunningTask() {
try {
var value1 = yield step1();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
```
然后,使用一个函数,按次序自动执行所有步骤。
```javascript
scheduler(longRunningTask());
function scheduler(task) {
setTimeout(function() {
var taskObj = task.next(task.value);
// 如果Generator函数未结束就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}, 0);
}
```
注意yield语句是同步运行不是异步运行否则就失去了取代回调函数的设计目的了。实际操作中一般让yield语句返回Promise对象。
```javascript
var Q = require('q');
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(deferred.resolve, milliseconds);
return deferred.promise;
}
function* f(){
yield delay(100);
};
```
上面代码使用Promise的函数库Qyield语句返回的就是一个Promise对象。
多个任务按顺序一个接一个执行时yield语句可以按顺序排列。多个任务需要并列执行时比如只有A任务和B任务都执行完才能执行C任务可以采用数组的写法。
```javascript
function* parallelDownloads() {
let [text1,text2] = yield [
taskA(),
taskB()
];
console.log(text1, text2);
}
```
上面代码中yield语句的参数是一个数组成员就是两个任务taskA和taskB只有等这两个任务都完成了才会接着执行下面的语句。
### 3部署iterator接口
利用Generator函数可以在任意对象上部署iterator接口。
```javascript
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
```
上述代码中myObj是一个普通对象通过iterEntries函数就有了iterator接口。也就是说可以在任意对象上部署next方法。
下面是一个对数组部署Iterator接口的例子尽管数组原生具有这个接口。
```javascript
function* makeSimpleGenerator(array){
var nextIndex = 0;
while(nextIndex < array.length){
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
```
### 4作为数据结构
Generator可以看作是数据结构更确切地说可以看作是一个数组结构因为Generator函数可以返回一系列的值这意味着它可以对任意表达式提供类似数组的接口。
```javascript
function *doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
```
上面代码就是依次返回三个函数但是由于使用了Generator函数导致可以像处理数组那样处理这三个返回的函数。
```javascript
for (task of doStuff()) {
// task是一个函数可以像回调函数那样使用它
}
```
实际上如果用ES5表达完全可以用数组模拟Generator的这种用法。
```javascript
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
```
上面的函数可以用一模一样的for...of循环处理两相一比较就不难看出Generator使得数据或者操作具备了类似数组的接口。