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);
注解:
- 此处函数体并不会被执行,仅仅是生成Generator对象;
- Generator对象被执行,函数体执行到yield或return关键字
稍复杂一点(Generator的内置通信方式)
function* secondFunc() {
var name = yield '你的名字是?';
return '我的名字是' + name;
}
var gen = secondFunc();
gen.next(); // 1
gen.next('就不告诉你'); // 2
注解:
- 函数执行至yield "你的名字是?",并且gen.next()本身返回一个对象,包含done和value两个属性值,done表示当前gen是否已经执行完毕,value表示yielded/returned的值,如此例中done为false, value为"你的名字是?"
- 再次执行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
}
注解:
- for of 循环,其实就是一直调用fibGen(10).next来获取值,并判断结束条件;
- 依次输出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);
})
注解:
- 调用next回调,函数签名为function(err, ret),这也是异步函数的通用回调函数签名;
- 当异步函数发生错误时,使用throw函数将错误抛出;
- 将获取到的异步函数返回的数据当成下一次gen.next值传入,即把返回数据当成yield语句的返回数据;
- 借用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值,koa 和 koa中文站 是基于Generator,类似于express
的web框架,被express作者TJ大神称为下一代web框架,也是TJ大神宣布离开node领域后唯一不肯放弃维护的一个库,魅力可见一斑~
链接:
- http://strongloop.com/strongblog/how-to-generators-node-js-yield-use-cases/
- http://zhuanlan.zhihu.com/thefrontendperiodicals/19750470
- https://github.com/visionmedia/co
- http://koajs.cn/
- http://cnodejs.org/topic/53d230358ed6f4002b3a89e7
听我唠叨了这么多,怎么样,想试试Generator? 来吧,让我们愉快的搞G吧!!!