1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-25 19:22:21 +00:00
es6tutorial/docs/generator.md
2014-12-26 05:44:02 +08:00

530 lines
16 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函数作用就是可以完全控制内部状态的变化依次遍历这些状态。
在形式上Generator是一个普通函数但是有两个特征。一是function命令与函数名之间有一个星号二是函数体内部使用yield语句定义遍历器的每个成员即不同的内部状态yield语句在英语里的意思就是“产出”
```javascript
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
```
上面代码定义了一个Generator函数helloWorldGenerator它的遍历器有两个成员“hello”和“world”。调用这个函数就会得到遍历器。
当调用Generator函数的时候该函数并不执行而是返回一个遍历器可以理解成暂停执行。以后每次调用这个遍历器的next方法就从函数体的头部或者上一次停下来的地方开始执行可以理解成恢复执行直到遇到下一个yield语句为止。也就是说next方法就是在遍历yield语句定义的内部状态。
```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方法。
第一次调用函数开始执行直到遇到第一句yield语句为止。next方法返回一个对象它的value属性就是当前yield语句的值hellodone属性的值false表示遍历还没有结束。
第二次调用函数从上次yield语句停下的地方一直执行到下一个yield语句。next方法返回的对象的value属性就是当前yield语句的值worlddone属性的值false表示遍历还没有结束。
第三次调用函数从上次yield语句停下的地方一直执行到return语句如果没有return语句就执行到函数结束。next方法返回的对象的value属性就是紧跟在return语句后面的表达式的值如果没有return语句则value属性的值为undefineddone属性的值true表示遍历已经结束。
第四次调用此时函数已经运行完毕next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法返回的都是这个值。
总结一下Generator函数使用iterator接口每次调用next方法的返回值就是一个标准的iterator返回值有着value和done两个属性的对象。其中value是yield语句后面那个表达式的值done是一个布尔值表示是否遍历结束。
由于Generator函数返回的遍历器只有调用next方法才会遍历下一个成员所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志next方法遇到yield就会暂停执行后面的操作并将紧跟在yield后面的那个表达式的值作为返回对象的value属性的值。当下一次调用next方法时再继续往下执行直到遇到下一个yield语句。如果没有再遇到新的yield语句就一直运行到函数结束将return语句后面的表达式的值作为value属性的值如果该函数没有return语句则value属性的值为undefined。另一方面由于yield后面的表达式直到调用next方法时才会执行因此等于为JavaScript提供了手动的“惰性求值”Lazy Evaluation的语法功能。
yield语句与return语句有点像都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield函数暂停执行下一次再从该位置继续向后执行而return语句不具备位置记忆的功能。一个函数里面只能执行一次或者说一个return语句但是可以执行多次或者说多个yield语句。正常函数只能返回一个值因为只能执行一次returnGenerator函数可以返回一系列的值因为可以有任意多个yield。
Generator函数可以不用yield语句这时就变成了一个单纯的暂缓执行函数。
```javascript
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
```
上面代码中函数f如果是普通函数在为变量generator赋值时就会执行。但是函数f是一个Generator函数就变成只有调用next方法时函数f才会执行。
## 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开始递增。
再看一个例子。
```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方法。
## 应用
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控制流管理
总结一下如果某个操作非常耗时可以把它拆成N步。
```javascript
function* longRunningTask() {
yield step1();
yield step2();
// ...
yield stepN();
}
```
然后,使用一个函数,按次序自动执行所有步骤。
```javascript
scheduler(longRunningTask());
function scheduler(task) {
setTimeout(function () {
if (!task.next().done) {
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对象。
### 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方法。
### 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使得数据或者操作具备了类似数组的接口。
## 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捕获。
这种函数体内捕获错误的机制,大大方便了对错误的处理。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数写一个错误处理语句。
```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);
}
```
## 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*语句遍历完全二叉树。
```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() {
···
}
};
```
它的完整形式如下,两者是等价的。
```javascript
let obj = {
myGeneratorMethod: function* () {
···
}
};
```