搞定 koa 之 Nodejs co源码

搞定 koa 之 Nodejs 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


5 回复

搞定 koa 之 Nodejs 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) {
  var isGenFun = isGeneratorFunction(fn);

  return function (done) {
    var ctx = this;

    var gen = fn;

    if (isGenFun) {
      var args = Array.prototype.slice.call(arguments);
      var hasCallback = args.length && 'function' == typeof args[args.length - 1];
      done = hasCallback ? args.pop() : error;
      gen = fn.apply(this, args);
    } else {
      done = done || error;
    }

    next();

    function exit(err, res) {
      setImmediate(function(){
        done.call(ctx, err, res);
      });
    }

    function next(err, res) {
      var ret;

      if (arguments.length > 2) res = Array.prototype.slice.call(arguments, 1);

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

      if (!err) {
        try {
          ret = gen.next(res);
        } catch (e) {
          return exit(e);
        }
      }

      if (ret.done) return exit(null, ret.value);

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

      if ('function' == typeof ret.value) {
        var called = false;
        try {
          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;
      }

      next(new TypeError('You may only yield a function, promise, generator, array, or object, but the following was passed: "' + String(ret.value) + '"'));
    }
  }
}
  • 判断是否为 generatorFunction:通过 isGeneratorFunction 方法。
  • 生成器函数的处理:如果 fn 是一个生成器函数,则将 arguments 转换成数组,并检查最后一个参数是否为回调函数。
  • 执行生成器函数:使用 fn.apply(this, args) 来执行生成器函数。
  • 递归调用 next 函数:通过 next() 来逐步执行生成器函数。
  • 处理异步操作:如果 ret.value 是一个函数,则调用该函数并传入回调函数,当异步操作完成时,回调函数会触发 next 函数,从而继续执行生成器函数。

希望这些解释和代码示例能帮助你更好地理解 co 的工作原理。


说2点markdown用法吧

  • 标题里的## 后面要加一个空格,这是因为cnodejs用的markdown编译器不支持不带空格的写法
  • 不要从2级标题【参考资料】跳到4级标题【联系我】,标题要有连续性

http://purplebamboo.github.io/2014/05/24/koa-source-analytics-1/ 我也写过一个系列,跟楼主一样。。按照 generator co koa的顺序 挨个的介绍。。

受教了

在深入理解 co 源码之前,我们需要先回顾一下 co 的基本用途。co 的核心功能是将 Generator 函数转换为同步的流程,从而简化异步操作。接下来我们将逐步解析 co 源码。

源码解析

首先,定义 co 函数:

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

  return function (done) {
    var ctx = this;

    var gen = fn;

    // 如果是 Generator 函数,则处理参数
    if (isGenFun) {
      var args = Array.prototype.slice.call(arguments, 0, -1);
      done = arguments[arguments.length - 1];
      gen = fn.apply(this, args);
    } else {
      done = done || error;
    }

    // 开始递归调用 next
    next();

    // 处理错误退出
    function exit(err, res) {
      setImmediate(function(){
        done.call(ctx, err, res);
      });
    }

    // next 函数递归调用
    function next(err, res) {
      var ret;

      if (err) {
        try {
          ret = gen.throw(err);
        } catch (e) {
          return exit(e);
        }
      } else {
        try {
          ret = gen.next(res);
        } catch (e) {
          return exit(e);
        }
      }

      if (ret.done) return exit(null, ret.value);

      // 将 yield 的值转为 Thunk 形式
      ret.value = toThunk(ret.value, ctx);

      if ('function' == typeof ret.value) {
        var called = false;
        try {
          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;
      }

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

// 假设 isGeneratorFunction 和 toThunk 已经实现

示例代码

我们来看一个具体的使用案例:

const co = require('co');

function read(file) {
  return function(callback) {
    fs.readFile(file, 'utf8', callback);
  };
}

co(function *() {
  const a = yield read('.gitignore');
  const b = yield read('Makefile');
  const c = yield read('package.json');
  console.log(a.length);
  console.log(b.length);
  console.log(c.length);
});

解释

  1. Generator 函数:在 co 中,yield 后面的表达式必须是可以被 next 调用的对象,如函数、Promise、Generator、数组或对象。
  2. co 函数co 接收一个 Generator 函数,并返回一个接受 done 回调的函数。co 通过递归调用 next 函数,逐步执行 Generator 函数中的每一个 yield 表达式。
  3. 错误处理:在每次调用 gen.next()gen.throw() 时,都会捕获可能的异常并调用 exit 函数进行错误处理。
  4. 异步处理:对于 yield 返回的函数,co 会将其包装成一个回调函数并立即调用,从而确保异步操作的正确执行。

通过以上代码和解析,你可以更好地理解 co 如何将异步操作串行化并简化代码编写。

回到顶部