Description
起因
最近在看一些node项目的时候发现里面用到了ES6的generator函数,yield和tj的co库,花了一些时间搞明白它们之间的关系,下面用一些例子说明。
溯源
对于异步的操作,最常规的写法是回调函数,但是深度回调会出现可怕的金字塔。那么,如何用更好的书写方式来避免金字塔,又或者说,怎么样把异步的代码写得看起来好像同步那样子呢?
其中一种解决方案是promise模式,.then一直then下去。ok,从ES6开始,有两个新的特性,叫generator和yield,借助它们,我们能够更优雅地解决这个问题。
generator和yield简介
请看下面的代码
function* Hello(){
yield 1;
yield 2;
}
var hello = Hello();
console.log(hello.next()); // { value:1, done:false }
console.log(hello.next()); // { value:2, done:false }
console.log(hello.next()); // { value:undefined, done:true }
- function后面的*号代表这是一个generator函数,而非普通函数,只有在generator函数中才能使用yield,在普通函数中使用yield会报错。
- generator函数的函数是分段的。第一次执行next的时候,程序会执行到第一个yield,然后返回{ value:1, done:false },表示yield后面返回1,但是函数Hello还没执行完,函数既不会退出,也不会往下执行。
- 当再次执行next的时候,从上次中断的地方接着执行,直到下一个yield或者函数结尾。
正是这种在单个函数内分步执行性质的引入,使得我们能够通过它来完成异步操作的"优化"。
假设有这样的例子
function delay(time, cb){
setTimeout(function(){
cb && cb()
},time);
}
delay(200,function(){
console.log('200ms done');
delay(1000,function(){
console.log('1200ms done');
delay(500,function(){
console.log('finish');
});
});
});
如何优化这个例子呢?
思路:根据generator的特性,如果我构造一个generator函数包含这三个异步操作,并且把他们各自的callback函数都设置为执行next()函数,这样不就可以实现"看起来是同步"的了吗?
function cl(){
yieldDelay.next();
}
function* YieldDelay(){
yield delay(3200,cl);
console.log('3200ms done!');
yield delay(4400,cl);
console.log('4400ms done!');
yield delay(5500,cl);
console.log('5500ms done!');
}
var yieldDelay = YieldDelay();
yieldDelay.next();
ok。我们已经迈出了一大步了。不过这个写法看着还是有些别扭。
- 第一次执行需要我手动出发next()函数。
- 回调函数只是简单地执行next()函数,为什么不能把它更加抽象化,以至于不用定义这个回调函数呢?
让我们先激动一小会儿,因为你在走tj大神曾经走过的路!
进一步优化这段代码
我们先想想思路,到底有什么办法能够做到呢?最开始的写法之所以会导致金字塔现象,是因为:函数a的执行里面包含执行函数b,所以函数b的执行里面也必须包含执行函数c……如果我们在函数a执行的时候只返回一个function,而这个function接收函数b作为参数。ok,我们先按照这个思路改造一下delay函数和generator函数
function delay(time){
return function(fn){
setTimeout(function(){
fn();
},time)
}
}
co(function* (){
yield delay(4200);
yield delay(4000);
yield delay(3000);
})(function(){
// 回调函数
console.log('all done!');
})
function co(GenFunc){
return function(cb){
//......先略过
}
}
我们分析一下:
- co函数接收generator函数作为参数,然后返回一个函数,该函数接收回调函数。
- delay函数接收时间作为参数,返回一个函数,该函数接收回调函数。
再次理一下思路,我们应该如何编写//........先略过这一部分的内容呢?
yield特性可以让我们分阶段执行,暂停→开始→暂停→开始……**如果我们可以让第一次执行的结果是一个函数,而这个函数接收第二次执行本身作为cb函数,第二次执行的结果也是一个函数,而这个函数接收第三次执行本身作为cb函数……直到结束。好吧,说再多还不如来几行代码!
function co(GenFunc) {
return function(cb) {
var gen = GenFunc(); // 第一次执行的时候构造出对象
next() // 调用自定义的next方法
function next() {
var ret = gen.next();
// 在generator函数中走一步,delay函数返回一个函数赋给ret.value
if (ret.done) {
// 判断ret.done是否为真,如果为真,说明generator函数执行完了,该调用回调函数了
cb && cb();
} else {
// 如果ret.done为假,那么调用上一个返回的函数,并且把next函数传递给它作为回调函数
ret.value(next);
}
}
}
}
嗯,看起来有点绕,多看几遍就好了。
至此,你已经山寨了一个极其简单的co库。
当然tj的co库比这个复杂多了,但是原理就是这样,还可以传参数,支持promise
遗留问题:
- 该看看ES6原生支持的promise对象了。
- generator+co这样的模式确实可以优雅地解决金字塔问题,不过ES7中提供async函数,利用它,不需要依赖co库,也一样可以解决这个问题。
参考资料: