1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/generator.md
2014-10-05 19:17:48 +08:00

12 KiB
Raw Blame History

Generator 函数

含义

所谓Generator简单说就是一个内部状态的遍历器即每调用一次遍历器内部状态发生一次改变可以理解成发生某些事件。ES6引入Generator函数作用就是可以完全控制内部状态的变化依次遍历这些状态。

Generator函数就是普通函数但是有两个特征。一是function关键字后面有一个星号二是函数体内部使用yield语句定义遍历器的每个成员即不同的内部状态yield语句在英语里的意思就是“产出”


function* helloWorldGenerator() {
    yield 'hello';
    yield 'world';
	return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个Generator函数helloWorldGenerator它的遍历器有两个成员“hello”和“world”。调用这个函数就会得到遍历器。

当调用Generator函数的时候该函数并不执行而是返回一个遍历器可以理解成暂停执行。以后每次调用这个遍历器的next方法就从函数体的头部或者上一次停下来的地方开始执行可以理解成恢复执行直到遇到下一个yield语句为止。也就是说next方法就是在遍历yield语句定义的内部状态。


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函数的本质其实是提供一种可以暂停执行的函数。yield语句就是暂停标志next方法遇到yield就会暂停执行后面的操作并将紧跟在yield后面的那个表达式的值作为返回对象的value属性的值。当下一次调用next方法时再继续往下执行直到遇到下一个yield语句。如果没有再遇到新的yield语句就一直运行到函数结束将return语句后面的表达式的值作为value属性的值如果该函数没有return语句则value属性的值为undefined。

由于yield后面的表达式直到调用next方法时才会执行因此等于为JavaScript提供了手动的“惰性求值”Lazy Evaluation的语法功能。

yield语句与return语句有点像都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield函数暂停执行下一次再从该位置继续向后执行而return语句不具备位置记忆的功能。

Generator函数可以不用yield语句这时就变成了一个单纯的暂缓执行函数。


function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next() 
}, 2000);

上面代码中只有调用next方法时函数f才会执行。

利用Generator函数可以在任意对象上部署iterator接口。


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

上述代码中由于Generator函数返回一个具有iterator接口的对象所以只要让yield语句每次返回一个参数对象的成员就可以在任意对象上部署next方法。

next方法的参数

yield语句本身没有返回值或者说总是返回undefined。next方法可以带一个参数该参数就会被当作上一个yield语句的返回值。


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开始递增。

再看一个例子。


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方法开始参数才是有效的。

异步操作的应用

Generator函数的这种暂停执行的效果意味着可以把异步操作写在yield语句里面等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了因为异步操作的后续操作可以放在yield语句下面反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用来处理异步操作改写回调函数。


function* loadUI() { 
	showLoadingScreen(); 
	yield loadUIDataAsynchronously(); 
	hideLoadingScreen(); 
} 
var loader = loadUI();
// 加载UI
loader.next() 

// 卸载UI
loader.next()

上面代码表示第一次调用loadUI函数时该函数不会执行仅返回一个遍历器。下一次对该遍历器调用next方法则会显示Loading界面并且异步加载数据。等到数据加载完成再一次使用next方法则会隐藏Loading界面。可以看到这种写法的好处是所有Loading界面的逻辑都被封装在一个函数按部就班非常清晰。

Ajax是典型的异步操作通过Generator函数部署Ajax操作可以用同步的方式表达。


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函数逐行读取文本文件。


function* numbers() {
	let file = new FileReader("numbers.txt");
	try {
		while(!file.eof) {
			yield parseInt(file.readLine(), 10);
		}
	} finally {
		file.close();
	}
}

上面代码打开文本文件使用yield语句可以手动逐行读取文件。

总结一下如果某个操作非常耗时可以把它拆成N步。


function* longRunningTask() {
	yield step1();
	yield step2();
	// ...
	yield stepN();
}

然后,使用一个函数,按次序自动执行所有步骤。


scheduler(longRunningTask());

function scheduler(task) {
	setTimeout(function () {
		if (!task.next().done) {
			scheduler(task);
        }
    }, 0);
}

注意yield语句是同步运行不是异步运行否则就失去了取代回调函数的设计目的了。实际操作中一般让yield语句返回Promise对象。


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对象。

for...of循环

for...of循环可以自动遍历Generator函数且此时不再需要调用next方法。


function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var 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循环实现斐波那契数列的例子。


function* fibonacci() {
    let [prev, curr] = [0, 1];
    for (;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}

for (n of fibonacci()) {
    if (n > 1000) break;
    console.log(n);
}

从上面代码可见使用for...of语句时不需要使用next方法。

yield*语句

如果yield命令后面跟的是一个遍历器需要在yield命令后面加上星号表明它返回的是一个遍历器。这被称为yield*语句。


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语句得到的值,是一个遍历器,所以要用星号表示。

下面是一个稍微复杂的例子使用yield*语句遍历完全二叉树。


// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
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']