ES6 的 Generator

#javascript #nodejs

Generator 是ECMAScript Harmony (ES6) 引入的新特性,通过该特性可以改变异步代码结构,提高代码的可读性。

创建Generator函数

function* Demo () {     // function后面带上*号
    yield "hello";      // yield中断函数执行并返回值
    yield "world";
    return "!";
}
var demo = Demo();              // 需要实例化
console.log (demo.next());      // { value: 'hello', done: false }
console.log (demo.next());      // { value: 'world', done: false }
console.log (demo.next());      // { value: '!', done: true }

可以看出, 通过yield关键字,可以中断函数的执行,并返回一个对象。{ value: 'hello', done: false} 其中done字段表示函数是否执行完毕。每次调用next方法就可以继续执行yield下面的代码。

再看一个复杂一点的例子:

function* Demo (num) {
    var result = num;
    var base = 0;
    base = yield result;        // result = 5

    console.log (base);         //2. 输出2, base的值等于第二次next调用的参数
    result *= base;             // result = 5 * 2
    base = yield result;

    console.log (base);         //4. 输出3,base的值等于第三次next调用的参数
    result *= base;             // result = 10 * 3
    return result;
}

var demo = Demo(5);
console.log (demo.next(1));     // 1. 输出 { value: 5, done: false }
console.log (demo.next(2));     // 3. 输出 { value: 10, done: false }
console.log (demo.next(3));     // 5. 输出 { value: 30, done: true }

/* 输出依次是:
{ value: 5, done: false }
2
{ value: 10, done: false }
3
{ value: 30, done: true }
*/

从上述例子可以得出以下结论:

  1. Generator函数同样可以传递参数。

  2. yield表达式的值是等于后一次调用next方法传入的参数值。(可以理解为:执行表达式yield result后,函数立刻中断并返回第一次next()的结果, 当第二次调用next的时候,函数继续执行并将传入的参数将作为表达式yield result的值。)

  3. 函数最后returnyield实现一样的功能,并且返回结果中的done等于true。 (建议尝试把return语句去掉看看会发生什么。)


好,至此已经说完了Generator的使用,那Generator跟异步调用究竟有什么关系?

先说需求:执行异步任务A,B,C,要求执行完A之后得到的结果作为参数传递给任务B,任务B完成之后再将任务B的结果传递给任务C去执行。 这里为了方便,假设A,B,C都是同一类的任务,定义函数如下:

/*
 *  异步任务,传入时间time,callback返回结果time
 */
function somethingAsync (time, callback) {
    setTimeout (function (){
        var err = null;
        console.log ("delay: " + time + "ms");
        if (time >= 10000) {
            err = new Error('timeout');
        }
        callback(err, time);
    }, time);
}

一般异步的代码是这样(callback hell):

// task A: 1000ms
somethingAsync(1000, function (err, time) {
    if (err) {
        throw new Error(err);
    }   
    // task B: (1000 + 100)ms
    somethingAsync(time + 100, function (err, time) {
        if (err) {
            throw new Error(err);
        }   
        // task C: (1000 + 100 + 500)ms
        somethingAsync(time + 500, function (err, time) {
            if (err) {
                throw new Error(err);
            }   
            console.log ("done!");
        }); 
    }); 
});

可以看出,异步代码会出现层层嵌套的结构,假如再多几个任务D,E,F,G,代码会变得难以阅读和维护(现在很多开源模块都能改善这个问题,例如:async, Q, wind.js, then.js 等)。引入Generator之后,可以利用yield中断特性改变代码结构:

wrap(function *() {
    var time = 1000;
    time = yield somethingAsyncWrap (time);         // task A: 1000
    time = yield somethingAsyncWrap (time + 100);   // task B: 1000 + 100
    time = yield somethingAsyncWrap (time + 500);   // task C: 1000 + 100 + 500
    return time;;
})(function (err, time){
    if (err) {
        console.error (err);
    } else {
        console.log ("done! time: " + time);    // done!
    }   
});

以上是改造完后的逻辑代码,通过wrap函数,消除了层层嵌套的callback结构,方便阅读以及维护。

以下是wrap函数的粗略实现:

/*
 * 定义wrap函数,利用Generator的特性消除callback嵌套
 */
function wrap (Gen) {
    var gen = new Gen();

    // 返回一个函数,该函数传入callback,当最后一个异步操作完成后就会触发该callback。
    return function (callback) {
        var next = function (result) {
            var fn = result.value;
            if (result.done) {  // 已经完成所有yield中断,最后一个异步任务完成
                callback (null, result.value);
            }   
            else {
                // 执行异步任务
                fn (function (err, val) {
                    if (err) {
                        callback (err);
                    }   
                    else {
                        // 完成后以返回结果作为参数调用下一次yield(下一个任务)
                        result = gen.next (val);
                        // 递归实现
                        next (result);
                    }   
                }); 
            }   
        };  
        next (gen.next());
    };  
} 

除此之外,需要对原本异步函数进行封装,以便适应wrap的调用。(这是这种方法的一个缺点: 需要对每个异步函数封装)

/* 封装原本的异步函数使其适应wrap函数的调用。*/
function somethingAsyncWrap (time) {
    return function (callback) {
        somethingAsync (time, callback);
    }   
}

到此为止, 我们实现了利用Generator的特性对异步代码结构进行优化。

其中,上述的wrap函数其实是TJ大神开源项目co的粗略版,建议大家去阅读一下大神的源码。 另外,TJ大神还有thunkify用于封装异步函数。 co是新一代框架koa的核心,建议node.js的朋友去了解一下。 co example


P.S: 以上代码基于Node v0.11.13 harmony模式运行。

$ node --harmony

只有v0.11.*的版本才支持Generator的特性。