搞定 koa 之 co源码解析

书接上文,这次我们来详细看看 co 的源码,这是了解 koa 的必要步骤。

系列目录

  1. 搞定 koa 之generator 与 co
  2. 搞定 koa 之 co源码解析

在看源码前,我们再看段 generator 的代码

function* Gen(a){  
  var b = yield a;
  console.log(b);//3
  var c = yield b;
  yield c;
}
var g = Gen(1);  
console.log(g.next(2));//{ value: 1, done: false }  
console.log(g.next(3));//{ value: 3, done: false }  
console.log(g.next(4));//{ value: 4, done: false }  
console.log(g.next());//{ value: undefined, done: true }  
  • 有点奇怪的结果,传入的2没有被返回
  • 调用 Gen 的时候,会为 a 初始化一个值,所以 g.next(2)返回1
  • 调用 g.next(3)的时候会为 b 赋值3,也就是说调用 g.next(2)的时候并没有为 b 赋值,只有再次调用 next,函数才会继续运行,执行到 b 赋值的部分。

看懂了上面的代码,再看看 co 是如何用的

var co = require('..');  
var fs = require('fs');

function read(file) {  
  return function(fn){
    fs.readFile(file, 'utf8', fn);
  }
}
co(function *(){  
  var a = yield read('.gitignore');
  var b = yield read('Makefile');
  var c = yield read('package.json');
  console.log(a.length);
  console.log(b.length);
  console.log(c.length);
});
  • co 需要传入一个 generatorFunction
  • co 的原理很简单,就是利用 next 逐步执行以及可以给 next 传参数来为变量赋值
  • 例如上面的代码,开始co执行generatorFunction,然后调用next,就可以获得 read('.gitignore')的返回值。这里的 read 会返回一个函数。
  • co 会往这个函数里面传入一个回调函数,readFile 完成时,回调函数会被触发,这个回调函数的逻辑就是调用 next(data)。这样 a 变量就会获得 readFile 返回的结果了。

看懂上面的代码,我们正式进入 co 的源码

function co(fn) {  
  //判断是否为 generatorFunction
  var isGenFun = isGeneratorFunction(fn);

  return function (done) {
    var ctx = this;

    // in toThunk() below we invoke co()
    // with a generator, so optimize for
    // this case
    var gen = fn;

    // we only need to parse the arguments
    // if gen is a generator function.
    if (isGenFun) {
      //把 arguments 转换成数组
      var args = slice.call(arguments), len = args.length;
      //根据最后一个参数是否为函数,判断是否存在回掉函数
      var hasCallback = len && 'function' == typeof args[len - 1];
      done = hasCallback ? args.pop() : error;
      //执行 generatorFunction 
      gen = fn.apply(this, args);
    } else {
      done = done || error;
    }
    //调用 next 函数,这是一个递归函数
    next();

    // #92
    // wrap the callback in a setImmediate
    // so that any of its errors aren't caught by `co`
    function exit(err, res) {
      setImmediate(function(){
        done.call(ctx, err, res);
      });
    }

    function next(err, res) {
      var ret;

      // multiple args
      if (arguments.length > 2) res = slice.call(arguments, 1);

      // error
      if (err) {
        try {
          ret = gen.throw(err);
        } catch (e) {
          return exit(e);
        }
      }

      // ok
      if (!err) {
        try {
          //执行 next,会获得 yield 返回的对象。同时通过 next 传入数据,为变量赋值
          //返回的对象格式是{value:xxx,done:xxx},这里的 value 是一个函数
          ret = gen.next(res);
        } catch (e) {
          return exit(e);
        }
      }

      // done 判断是否完成
      if (ret.done) return exit(null, ret.value);

      // normalize 
      ret.value = toThunk(ret.value, ctx);

      // run
      if ('function' == typeof ret.value) {
        var called = false;
        try {
          //执行 ret.value 函数,同时传入一个回调函数。当异步函数执行完,会递归 next
          //next又会执行gen.next(),同时把结果传出去
          ret.value.call(ctx, function(){
            if (called) return;
            called = true;
            next.apply(ctx, arguments);
          });
        } catch (e) {
          setImmediate(function(){
            if (called) return;
            called = true;
            next(e);
          });
        }
        return;
      }

      // invalid
      next(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following was passed: "' + String(ret.value) + '"'));
    }
  }
}

参考资料

co

联系我

微博 GitHub