Skip to content
On this page

co 的实现

co 的实现非常精巧,严重推荐阅读 co 的源码。

co 代码优化过,新的4.x.x的代码基于Promise重构过,比以前的代码好理解很多。

上一个版本的可以看3.1.0的代码,很晦涩,不好理解。

4.x.x 的 api 有些变化,co 返回的是一个 promise,这部分的内容留到最后一节讲解。

一个最简单的 co 实现:

javascript
function co(fn) {
        return function(done) {
            var ctx = this;
            var gen = fn.call(ctx);
            var it = null;
            function _next(err, res) {
                if(err) res = err;
                it = gen.next(res);
                //{value:function(){},done:false}
                if(!it.done){
                    it.value(_next);
                }
            }
            _next();
        }
    }

调用:

javascript
var fs = require('fs');
    //一个 thunk 函数
    function read(file) {
        return function(fn){
            fs.readFile(file, 'utf8', fn);
        }
    }
    co(function *(){
        var c = 2;
        console.log(c);
        var a = yield read('error.js');
        console.log(a.length);
    
        var b = yield read('package.json');
        console.log(b.length);
    })();

请自行复制以上代码,并给觉得有疑惑的地方打上断点,查看逻辑的执行流程。

我们需要解开如下几个关键问题:

  • 问题1:为什么异步函数需要封装成 thunk 偏函数的形式?
  • 问题2:var a = yield read('error.js'); 为什么 a 的值是 read() 异步返回的数据?
  • 问题3:co 接受的 generator function 内部执行逻辑与co内部逻辑的执行顺序是什么样的?

(PS: yield 后的内容称之为 yieldable)

var gen = fn.call(ctx); fn 是co的实参,为一个generator function,再次强调 generator function 调用时,只定义不执行,与普通函数不同。

co 的核心是想办法递归调用 generator function 的 next(),直到执行完毕,_next() 就是个这样的递归方法:

javascript
function _next(err, res) {
        if(err) res = err;
        it = gen.next(res);
        //{value:function(){},done:false}
        if(!it.done){
            it.value(_next);
        }
    }
    
    _next();

第一次调用 _next() ,执行到 gen.next(res); 开始执行 generator function 遍历器的逻辑:

javascript
var c = 2;
    console.log(c);
    yield read('error.js');

留意不是执行到 var a = 给a赋值(赋值是在 yield 逻辑之后)的逻辑,遍历器执行到 yield 关键字就中断了,开始执行 read 这个 thunk 的逻辑。

read是一个偏函数,只做一件事情,返回一个函数:

javascript
function(fn){
        fs.readFile(file, 'utf8', fn);
    }

核心问题:这里的 fn 的实参是什么?

执行完 read() 后,执行 it = (co中的 it = gen.next(res); )的赋值,it的值是:

javascript
{value:function(fn){
       fs.readFile(file, 'utf8', fn);
    },done:false}

(参见第一节 generator)value 即偏函数 return 的函数,因为遍历器知道后面还有 yield 标识,也就值没有执行完,所以 done 为 false。

接下来我们执行 value 函数:

javascript
if(!it.done){
        it.value(_next);
    }

解答前面的核心问题,function(fn){fs.readFile(file, 'utf8', fn);} fn 指的是 _next 遍历方法。

正式执行异步逻辑获取文件数据:fs.readFile(file, 'utf8', fn); 且执行完后,执行 _next()。

所以 _next 带有二个形参:err, res,这是 fs.readFile(err,res) 的回调函数的形参。

res 就是读取文件后返回的文件内容。

这就解答了前面的 第一个问题,为什么异步函数需要封装成 thunk 偏函数的形式,因为我们需要在异步执行完成后,触发 _next 遍历方法。

_next 进入第二次调用,又执行了次 gen.next(res) 将 res 塞回到遍历器内部,且遍历器执行第二段逻辑:

javascript
var a = res;
    console.log(a.length);
    yield read('package.json');

这就解答了前面的 第二个问题,var a = yield read('error.js'); 为什么 a 的值是 read() 异步返回的数据?那是因为 a 的值是 gen.next() 塞回去的。

执行到 yield 关键字时,又中断了,执行 read 偏函数,it.value(_next) 执行偏函数的返回函数,异步读取文件内容。

读取完文件后,又再次触发了 _next() 方法 直到 done 为 true 时。

这就解答了 第三个问题:co 接受的 generator function 内部执行逻辑与co内部逻辑的执行顺序是什么样的?

当你搞清楚了这三个问题,就清楚了 co 的实现原理。