何方舟

2016-06-30 12:57

co.js 异步回调的原理

本文作者:IMWeb 何方舟 原文出处:IMWeb社区 未经同意,禁止转载

co.js 作为 koa 框架的核心库,利用 es6 Generator 新特性来解决 callback hell 已经非常流行 。 本文将剖析 co.js 是为何用同步的写法,就可以解决异步回调的问题。

Generator

首先简要介绍一下 Generator 特性, co.js 是基于该特性实现的,所以弄清 Generator 的远离非常重要。


function* fn(){
    beforeA();
    yield doA();
    yield doB();
    afterB();
}
var gen = fn(); //生成构造器;
gen.next(); //这里会执行到以第一个yield之前的位置,所以执行beforeA 和 doA 这两行;
gen.next(); //这里会执行到第二个yield的位置,也就是执行 doB() 
gen.next(); //这里会执行到生成器结束的位置,afterB();

简单来说 generator 可以变成一种分步函数,gen 成为这 Generator 函数的指针,通过调用 gen.next() 来执行下一步,这也是异步执行的关键。generator详细介绍请看这里 是不是有种感觉可以利用这个next来达到异步的,但是好像又不知道怎么该怎么去做,那先看看下面这个例子。

var fs = require("fs");
fs.readFile('path1', function (err, data) {
  if (err) throw err;
  console.log(data);
  fs.readFile('path2', function (err, data) {
    if (err) throw err;
        console.log(data);
  });
});

这是一个常见的异步回调的例子,现在我们用generator来改写它,下面是第一版。


var fs = require("fs");

function* unname(){

    var data1 = yield fs.readFile('path1',function(err,data){
        if(err) gen.throw(err);
        gen.next(data);
    });

    console.log(data1);

    var data2 = yield fs.readFile('path2',function(err,data){
        if(err) gen.throw(err);
        gen.next(data);

    }); 

    console.log(data2);
}

var gen = unname();

大功告成!可是好像哪里不对,这个本质上还是之前的回调方法。我们期望的方法应该是类似这样的,通过一个yield关键字,来表明这里是异步执行的。这样的写法简洁明了,但直接这样写肯定是不能执行的。


var fs = require("fs");

function* unname(){
    var data1 = yield fs.readFile('path1');
    console.log(data1);
    var data2 = yield fs.readFile('path2');
    console.log(data2);
}

为了达到这个目的,我们必须借助其他工具函数,这个就是Co。


var fs = require("fs");

co(function*(){
    var data1 = yield readFile('path1');
    console.log(data1);
    var data2 = yield readFile('path2');
    console.log(data2);
});

function readFile( path ){
    return function(callback){
       fs.readFile( path , callback);
    }
}

function co( fn ) {

    var gen = fn();
    function next(err,data){   
        var result = gen.next(data);
        if(!result.done){           
            result.value(next); 
        }
    }
    next();

}

上面的代码有两个关键点,一个是 readfile 函数的 thunk 化,还有就是 co 函数了,这里是最简单的实现。
网上很多教程都忽略了这一点,就是 Co 中需要流程控制的函数,都必须要 Thunk 化或者 Promise 化。因为 Promise 相对于 Thunk 要复杂一点,这里只介绍 Thunk 化。

所谓 Thunk 化就是将多参数函数,将其替换成单参数只接受回调函数作为唯一参数的版本 ,上面代码中的 readFile 就是个例子。
原生的api是不支持 thunk 化的,所以就有了thunkify这个库帮我们把一些原生 api thunk 化。

为什么要 thunk 化呢?由之前的分析我们可以知道,利用 generator 来实现异步回调的实质就是把, gen.next() 放入回调函数中, thunk 化之后,可以得到一个只接受 callback 的函数,换句话说,函数中除了 callback 其他都参数都已经传入了,callback 里的内容就可以交给 Co 去决定!

现在让我们来看下 Co 里面的代码。第一次执行 gen.next() 返回的 result.value 就是 fs.readFile thunk 化后的函数,就是这样的一个函数


function(callback){   
    fs.readFile( 'path1',callback );
}

通过result.value(next)中,我们就可以在callback中执行 gen.next(),翻译过来是这样。


function(callback){
    fs.readFile( 'path1', next );
}

这样就达到了我们想要异步执行的效果!

上面代码中的 Co 和 thunk 都是最简单的实现方式,代码中缺少诸如异常处理,非标准参数,多参数回调等判断,可以参考一下 Co 和 thunkify ,来实现。在 Co 的4.XX版本之后,内部的机制全部改为用 Promise 的实现,虽然看上去 Promise 是大势所趋,但是个人来说还是更喜欢Thunk的方式。等更深入学习 Promise 之后,会介绍 Promise 的实现方式。

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。