LeoYuan / leoyuan.github.io

This is my blog repo.

Home Page:http://leoyuan.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ES6 - Generator,未来,已来!

LeoYuan opened this issue · comments

注:原文发于公司内部的atatech,转载过来~

曾几何时,前端的代码是这样的,主要为同步代码:

var origVal = 1, newVal;

newVal = handle(origVal);

doSomethingWith(newVal);

异步代码也不过是事件监听或者ajax,或者单层嵌套

document.addEventListener('click', function() {
    alert('点我干嘛?!');

    $.ajax({
        success: function() { /* dom manipulation */ }
    });
}, false);

不过到了node时代,代码变成了这个样子:

var db = require('./db/callbackDb');

db.set('key1', 'value1', function(err) {
    if (err) throw err;

    db.set('key2', 'value2', function(err) {
        if(err) throw err;

        db.set('key3', 'value3', function(err) {
            if(err) throw err;

            var str = '';
            db.get('key1', function(err, value) {
                if(err) throw err;

                str += value + ' - ';

                console.log(str);
            });
        });
    });
});

这就是传说中的 paramid of doom ,一种窒息的感觉迎面扑来,有木有?!

不过因为一些前辈的聪明才智,想出了一些方式来解决这种金字塔厄运式的嵌套回调,如Promise、Async等异步流程库,虽然这些库在一定程度上缓解了层层嵌套的尴尬,不过代码的复杂度依然很高,可读性不佳,我们憧憬着,要是能把异步代码写的像同步代码那样,回到我们那个纯真年代多好!(尽管我们从不曾纯真过o(╯□╰)o)

是的,银弹来了,那就是Ecmascript 6中提出的Generator,何谓渣那瑞塔?不用急,听小弟慢慢儿道来~

什么是Generator?

简单的说Generator就是一个函数执行器,能够 挂起 执行,也能够在某个时间点 恢复继续 执行 ,Generator函数用 function* 定义,函数体内使用 yield 关键字来挂起,执行Generator函数后,得到一个Generator对象 ~

Generators are function executions that can be suspended and resumed at a later point; a
lightweight coroutine. This behavior happens using special generator functions (noted by function* > syntax) and a couple of new keywords (yield and yield*) which are only used in the context of a
generator.

注:以下代码都需要用node --harmony sample.js才可以运行

来个最简单的例子

function* firstFunc() {
    console.log('这是我的第一个gen,但是我并不会一开始就log出来');
}

var firstFuncGen = firstFunc();   // 1   

setTimeout(function() {
    firstFuncGen.next();       // 2
}, 1000);

注解:

  1. 此处函数体并不会被执行,仅仅是生成Generator对象;
  2. Generator对象被执行,函数体执行到yield或return关键字

稍复杂一点(Generator的内置通信方式)

function* secondFunc() {
    var name = yield '你的名字是?';
    return '我的名字是' + name;
}

var gen = secondFunc();

gen.next();    // 1

gen.next('就不告诉你');    // 2

注解:

  1. 函数执行至yield "你的名字是?",并且gen.next()本身返回一个对象,包含done和value两个属性值,done表示当前gen是否已经执行完毕,value表示yielded/returned的值,如此例中done为false, value为"你的名字是?"
  2. 再次执行gen.next(),程序继续往下跑,并且传入了值"就不告诉你",以此当做yield语句的返回值,此时返回对象done为true,value为"我的名字是就不告诉你"

来个有趣一点的例子(依次产生群众们喜闻乐见的斐波那契数列)

function* fibGen(n) {
    var i = 1, prev = 0, cur = 1, temp;
    while (i++ <= n) {
        yield cur;
        temp = cur;
        cur = prev + cur;
        prev = temp;
    }
}

for (var item of fibGen(10)) {   // 1
    console.log(item);       // 2
}

注解:

  1. for of 循环,其实就是一直调用fibGen(10).next来获取值,并判断结束条件;
  2. 依次输出1,1,2,3,5...

Generator异常机制

function* throwExpGen() {
    return 'err';
}

var throwExp = throwExpGen();

if (throwExp.next().value === 'err') {
    throwExp.throw('an err thrown from throwExpGen');
}

注解:一般异常机制可以结合node函数/promise对象的err/fail使用~


So,这就是我们说的Generator的全部内容啦?是的,它是~

总结一下,Generator就是一种通过 function* 关键字定义Generator Function执行后的对象,叫生成器,也可以叫做迭代器,在Generator Function中可以使用 yield / yield* 来设置程序执行挂起点,Generator对象通过next()函数执行类似在函数中一个一个断点跳转的逻辑,每次执行next(),都能返回一个对象,其中指明了 yielded / returned的值(next().value),以及当前Generator对象是否已经结束的值(next().done),还能通过 throw 函数抛出异常,另外next()还可传入参数,参数被当成是上一个yield语句的返回值~

试想一下,假如上边的例子yield的是一种已知的异步函数,即我们已经知道这种异步函数的回调机制,结合上边介绍的Generator能力,等待异步函数执行完毕,拿到result后用gen.next(result)返回,

var dirs = yield readdir('/etc'); 

我们岂不是可以获得一种将异步代码写成同步代码的能力?

如何更好的使用Generator?

试想一下,使用者不需要知道Generator的实现细节,不需要知道next调用,也不需要知道next()的返回值,更不需要根据done/value来控制程序执行流程,只需要预期yield的返回数据,直接使用,这样不就实现了所谓的将异步代码写成同步形式了么?

参考如下代码:

function readdir(dirName) {
    // 返回一个promise对象
    return aPromise;
}

function run(genFunc) {
    var gen = genFunc();
    next(); // 1

    function next(err, ret) {
        if (err) gen.throw(err);  // 2
        var genRet = gen.next(ret);  // 3
        if (genRet.done) return;

        if (isPromise(genRet.value)) {
            genRet.value.then(function(ret) { // 4
                next(null, ret);
            }, next);
        }
        // 这里还可以支持其他异步函数
        else if () {}
    }
}

run(function* () {
    var dirNames = yield readdir('/etc');
    console.log(dirNames);
})

注解:

  1. 调用next回调,函数签名为function(err, ret),这也是异步函数的通用回调函数签名;
  2. 当异步函数发生错误时,使用throw函数将错误抛出;
  3. 将获取到的异步函数返回的数据当成下一次gen.next值传入,即把返回数据当成yield语句的返回数据;
  4. 借用promise对象的then方法获取返回的数据ret或者失败的err,当返回ret时,把ret当成value传入下一次gen.next函数,当返回err时,抛出错误。

其实不仅仅是promise能够支持异步代码同步化的场景,理论上所有 具有明确回调调用方式async(next)以及回调函数签名function(err, ret) 的异步函数都能够在扩展run函数的实现上予以支持,有点抽象,用代码说话:

示例代码如下:

// promise
promise.then(function(ret) {
    next(null, ret);
}, next);

// node原生api
function toThunk(nodeFn) {
    // 以fs.readFile为例
    nodeFn('path/to/file', 'utf-8', function(err, content) {
        next(err, content);
    })
}

// 抽象出相同的异步调用模式
function thunkify(obj) {
    if (isPromise(obj)) {
        return function(next) {
             obj.then(function(ret) {
                  next(null, ret);
             }, next);
        }
    } 
    // node原生api
    else {
        return function(next) {
            // 以fs.readFile为例
            obj('path/to/file', 'utf-8', function(err, content) {
                next(err, content);
            });
        }
    }
}

将异步函数转化成 async(next) 这种格式就叫 thunkify,一般是对node原生api进行thunkify,而上面的通过传入done回调执行的异步函数async则被称为 thunk

thunkify示例代码:

function thunkify(nodeFn) {
    return function () {
        var args = Array.prototype.slice.call(arguments)
        return function (cb) {
          args.push(cb)
          nodeFn.apply(this, args)
        }
    }
}

更多的Generator相关资料

其实上面写的示例Generator流程控制函数run算是开源库 co 的一个简单实现,co能支持 function / promise / generator / array / object等类型的yield值,koakoa中文站 是基于Generator,类似于express的web框架,被express作者TJ大神称为下一代web框架,也是TJ大神宣布离开node领域后唯一不肯放弃维护的一个库,魅力可见一斑~

链接:


听我唠叨了这么多,怎么样,想试试Generator? 来吧,让我们愉快的搞G吧!!!