概念
这里是承接上一篇的Promise的,尽管Promise解决了“回调地狱”的问题和控制反转/可信任性的问题,但是它还是基于回调的。而ES6生成器(generator)就是来解决这个问题的,它提供了一种顺序的、看似同步的异步流程控制表达风格。
var x = 1;
function *foo() {
x++;
yield; // 暂停!
console.log( "x:", x );
}
这就是一段生成器代码,如果是简单的 foo() 这段代码是无法运行的,怎么执行呢。
var it = foo();
it.next();
it.next();
构造一个迭代器来控制生成器,然后执行next()。现在来讲讲这段代码的执行细节
- it = foo()运算并没有执行生成器*foo(),只是构造了一个迭代器(iterator),迭代器会控制它的执行
- 第一个 it.next() 启动了生成器 *foo(),并运行了 *foo() 第一行的 x++
- *foo() 在 yield 语句处暂停,在这一点上第一个 it.next() 调用结束。此时 *foo()仍在运行并且是活跃的,但处于暂停状态。
- 最后的 it.next() 调用从暂停处恢复了生成器 *foo() 的执行,并运行 console.log(..)语句,这条语句使用当前 x 的值 2。
所以,生成器是一类特殊的函数,可以一次或者多次启动和停止,但是不一定非得完成,取决于你的需求。
而关键字yield在这里表示的是暂停的意思,也就是生成器遇到了yield就会暂停但不会结束,直达下一个next的调用,如果没有yield,运行第一个next的时候,整个函数就会和普通函数一样执行完。
function *foo(x) {
var y = x * (yield);
return y;
}
var it = foo( 6 ); // 启动
console.log( it.next() )
console.log( it.next( 7 ) )
yield出现的位置是要去代码为yield提供一个结果值,而后续传入的7就是需要的值,故返回了42。
实际上,yield .. 和 next(..) 这一对组合起来,在生成器的执行过程中构成了一个双向消息传递系统。yield.. 作为一个 表达式可以发出消息响应 next(..) 调用,next(..) 也可以向暂停的yield 表达式发送值。而每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。
迭代器
迭代器是一个定 义良好的接口,用于从一个生产者一步步得到一系列值
var something = (function(){
var nextVal;
return {
// for..of循环需要
[Symbol.iterator]: function(){ return this; },// 标准迭代器接口方法
next: function(){
if (nextVal === undefined) {
nextVal = 1;
}else {
nextVal = (3 * nextVal) + 6;
}
return { done:false, value:nextVal };
}
};
})();
这个代码显示的是一个标准的迭代器的接口。它是用来实现数字序列生成器的。
[Symbol.iterator] 表示可迭代的,主要是可以用来让这个迭代器可循环。而其中的next是必须的,它返回一个对象,这个对象含有两个属性,done 是一个 boolean 值,标识迭代器的 完成状态;value 中放置迭代值。done为true时,表示迭代结束了。
迭代器和生成器的结合重要的一点是可以用来解决异步的问题。由于yield的存在,它使得异步的代码不会阻程序,它只是暂停或阻塞了生成器本身的代码。
function foo(x,y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function(err,data){
if (err) {
// 向*main()抛出一个错误
it.throw( err );
}
else {
// 用收到的data恢复*main()
it.next( data );
}
});
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
var it = main();
// 这里启动!
it.next();
这里的代码有一个异步的操作,但是后续的next操作却可以做到同步,这个是一个巨大的改进。
生成器+Promise
function foo(x,y) {
return request( //这里是个Promise
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
var text = yield foo( 11, 31 );
console.log( text );
}
catch (err) {
console.error( err );
}
}
运行方式如下
var it = main();
var p = it.next().value;
p.then(
function(text){
it.next( text );
},
function(err){
it.throw( err );
} );
但是实际上有很多第三方的库能帮助我们做很多东西,比如错误处理,自动迭代生成返回值等。asynquence,runner等都做到了。它们会自动异步运行你传给它的生成器,直到结束。
而后续 ES7的async 与 await,就是异步操作的最终解决方案。它其实就是迭代生成器和Promise的结合。
生成器委托
有时候,你可能会从一个生成器调用另一个生成器,这时候就需要yied委托。
function *foo() {
console.log( "*foo() starting" );
yield 3;
yield 4;
console.log( "*foo() finished" );
}
function *bar() {
yield 1;
yield 2;
yield *foo();
yield 5;
}
var it = bar();
it.next().value; //1
it.next().value; //2
it.next().value; //*foo()启动
it.next().value; // 4
it.next().value; // *foo()完成
//5
注意yield * __ ,这就是委托的语法。
这里,我们调用foo()创建一个迭代器。然后yield *把迭代器实例控制(当前 *bar() 生成器的)委托给 / 转移到了这另一个 *foo() 迭代器。
所以,前面两个 it.next() 调用控制的是 *bar()。但当我们发出第三个 it.next() 调用时, *foo() 现在启动了,我们现在控制的是 *foo() 而不是 *bar()。这也是为什么这被称为委 托:*bar() 把自己的迭代控制委托给了 *foo()。
一旦 it 迭代器控制消耗了整个 *foo() 迭代器,it 就会自动转回控制 *bar()。