生成器

96 阅读4分钟

概念

这里是承接上一篇的Promise的,尽管Promise解决了“回调地狱”的问题和控制反转/可信任性的问题,但是它还是基于回调的。而ES6生成器(generator)就是来解决这个问题的,它提供了一种顺序的、看似同步的异步流程控制表达风格。

var x = 1;
function *foo() {
    x++;
    yield; // 暂停!
    console.log( "x:", x );
}

这就是一段生成器代码,如果是简单的 foo() 这段代码是无法运行的,怎么执行呢。

var it = foo();
it.next();
it.next();

构造一个迭代器来控制生成器,然后执行next()。现在来讲讲这段代码的执行细节

  1. it = foo()运算并没有执行生成器*foo(),只是构造了一个迭代器(iterator),迭代器会控制它的执行
  2. 第一个 it.next() 启动了生成器 *foo(),并运行了 *foo() 第一行的 x++
  3. *foo() 在 yield 语句处暂停,在这一点上第一个 it.next() 调用结束。此时 *foo()仍在运行并且是活跃的,但处于暂停状态。
  4. 最后的 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()。