koa源码分析系列(二)co的实现
发布在js从零单排2015年1月28日view:2111cialisJavaScript
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。

  1. koa源码分析系列(一)generator
  2. koa源码分析系列(二)co的实现
  3. koa源码分析系列(三)koa的中间件机制实现
  4. koa源码分析系列(四)co-4.0新变化

koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。 有下面几种办法体验generator:

  • node v0.11 可以使用 (node —harmony)
  • 使用gnode来使用,不过据说性能一般
  • 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用,重启chrome即可。

thunk函数

thunk函数是一个偏函数,执行它会得到一个新的只带一个回调参数的函数。下面我们对node的stat举个例子(其实是co官方的例子):

var fs = require('fs');
function size(file) {
  return function(fn){
    fs.stat(file, function(err, stat){
      if (err) return fn(err);
      fn(null, stat.size);
    });
  }
}
var getIndexSize = size("./index.js");

getIndexSize(function(size){
    console.log(size);
})

size函数就是个典型的thunk函数了,执行size("./index.js")我们就会得到一个只有回调的新函数。co的异步解决方案需要建立在thunk的基础上。

使用co时,yield的经常是thunk函数,thunk函数可以使用一些方法转换,也有一些库支持,可以了解下thunkify 或者thunkify-wrap。

最简单的co实现

我们先看下有了co我们会怎么编程:

co(function *(){
  var a = yield size('.gitignore');
  var b = yield size('package.json');
  console.log(a);
  console.log(b);
  return [a,b];
})(function (err,args){
  console.log("callback===args=======");
  console.log(args);

})
//下面是结果,实际的数据根据你的文件会有不同
/*
12
1215
callback===args=======
[ 12, 1215 ]
*/

你会发现我们可以直接使用yield来直接获取 异步函数的值了。如果忽略yield关键字,完全就是同步编程了。再也不用考虑那一大堆回调了。co本质上也是一个thunk函数,接收一个generatorfunction作为参数,生成一个实际操作函数。这个实际操作函数可以接收一个callback来传入最后return的值。 下面我们就来实现最简单的co函数:

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

co本质上也是thunk函数,传入一个generatorFunction,它会自动帮你不停的调用对应generator的next函数,如果done为true代表generatorFunction函数执行完毕,就会把值传给回调函数。逻辑比较简单就不详细解释了。这边要注意_next函数的实现,注意11行,_next实际上会成为前面yield后面的函数的回调函数。 比如前面我们说的size('package.json')会返回一个带回调的函数a。于是调用就是yield a。这边11行it.value就会是这个a,会把_next作为回调执行a函数。 所以这边需要有个约定就是thunk函数的回调都要是function(err,res){}的格式,实际上这也是node实际的规范。

进阶-yield后面跟array或者对象

上面我们实现了一个最简单的co函数,已经可以支持最基本的同步调用了,但是yield后面只能跟thunk函数的执行结果。我们这边还需要支持其他类型的yield值,比如一个数组或者对象。 我们要对co做些改进:

function co(fn) {
  return function(done) {
    var ctx = this;
    var gen = fn.call(ctx);
    var it = null;
    function _next(err, res) {
      it = gen.next(res);
      if (it.done) {
        done.call(ctx, err, it.value);
      } else {
        //new line
        it.value = toThunk(it.value,ctx);
        it.value(_next);
      }
    }
    _next();
  }
}

35行,我们增加了一行it.value = toThunk(it.value,ctx);用于对yield的值进行处理。 我们看下toThunk的实现:

function isObject(obj){
  return obj && Object == obj.constructor;
}
function isArray(obj){
  return Array.isArray(obj);
}
function toThunk(obj,ctx){
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }
  return obj;
}

toThunk主要就是用来判断yield返回的值的类型,如果是对象或者数组就会调用objectToThunk对返回值做处理。否则的话就会正常的返回。

下面我们重点看看objectToThunk的实现方式。

function objectToThunk(obj){
  var ctx = this;
  return function(done){
      var keys = Object.keys(obj);
      var results = new obj.constructor();
      var length = keys.length;
      var _run = function(fn,key){
        fn.call(ctx,function(err,res){
          results[key] = res;
          --length || done(null, results);
        })
      }
      foreach(var i in keys){
        _run(Object[keys[i]],keys[i]);
      }

  }
}

其实这种类型的函数基本都是一个思路。都是将数组里面所有的thunk函数全部拿出来执行一次,通过记录下数组的长度,各个函数执行一次就对公用的长度变量减一,不需要关心各个函数的执行顺序,只要当其中一个函数发现变量变为0时,代表其他函数都执行好了,我是最后一个,于是就可以调用回调函数done了。 objectToThunk就是这种思路。 首先我们先解释下面这两句的意思:

 var keys = Object.keys(obj);
 var results = new obj.constructor();

这么写是为了通用性,Object.keys接收一个数组或者对象,返回key值。eg:

 Object.keys([1,2,3,4]) //[ '0', '1', '2', '3' ]
 Object.keys({"one":1,"two":2,"three":3}) //[ 'one', 'two', 'three' ]

然后new obj.constructor()这句,会根据obj的类型生成一个相关的空数组或者空对象。便于下面的赋值。这也是动态语言的优势。

之后我们定义了length变量,初始化为数组或者对象的属性长度。 然后就如上面的那个思路,挨个的使用_run执行每个函数,根据length来判断是否所有的函数都执行完毕了,执行完毕就调用回调函数done。

可以看到objectToThunk本质上也是一个thunk函数。这样 我们通过这层转换,使得数组里面的函数可以并行执行。

通过这层封装我们可以这么调用了:

co(function *(){
  var a = size('.gitignore');
  var b = size('package.json');
  var r = yield [a,b];
  return r;
})(function (err,args){
  console.log("callback===args=======");
  console.log(args);

})
/*
callback===args=======
[ 12, 1215 ]
*/

yield后面跟的数组,两个异步任务,将会并行执行,不在乎谁先结束,而是等最慢的一个执行完成后会得到返回值赋值给r。

有的时候,可能会发生数组里面还是数组的情况,我们需要深度遍历执行。所以我们需要对上面的_run函数做下改造:

var _run = function(fn,key){
    //new line
    fn = toThunk(fn);
    fn.call(ctx,function(err,res){
      results[key] = res;
      --length || done(null, results);
    })
}

只要加一句fn = toThunk(fn);就成功实现了深度遍历了。不得不说TJ的设计真是太强大。 这样 我们就可以这么调用了:

co(function *(){
  var a = [size('.gitignore'), size('index.js')];
  var b = [size('.gitignore'), size('index.js')];
  var c = [size('.gitignore'), size('index.js')];
  var d = yield [a, b, c];
  console.log(d);
})()

进阶-yield后面跟promise,或者generator或generatorFunction

co的强大之处在于,yield真的几乎什么都可以跟了。promise是我们经常使用的解决异步的东西。我们现在如果想要支持yield后面跟promise对象,只需要做点小改动就行。 首先在toThunk里面加点东西

function isPromise(obj) {
  return obj && 'function' == typeof obj.then;
}
function toThunk(obj,ctx){
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }
  if (isPromise(obj)) {
    return promiseToThunk.call(ctx, obj);
  }
  return obj;
}

是的,只需要加一个针对promise的判断就行了。然后通过promiseToThunk来转换promise。 promiseToThunk的实现也比较容易:

function promiseToThunk(promise){
    return function(done){
        promise.then(function(err,res){
            done(err,res);
        },done)
    }
}

还是通过转换,转成一个只有一个回调参数的函数。

那我们怎么去支持yield后面跟generator呢? 如果yield后面跟generator,我们期待的理想的结果是,继续执行这个generator里面的断点。其实有点类似es6规范里面yield的delegating yiled,不清楚的可以去看上一篇博文。co相当于做了这么个扩展。

首先我们继续在toThunk里面加一个判断

function isGenerator(obj) {
  return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
function toThunk(obj,ctx){
  if (isGenerator(obj)) {
    return co(obj);
  }
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }
  if (isPromise(obj)) {
    return promiseToThunk.call(ctx, obj);
  }
  return obj;
}

如果是generator的话 我们就直接调用co去处理。有木有觉得奇怪之前明明说co只接受generatorFunction来着。 别急,让我们对co函数做点小改动:

function co(fn) {
  return function(done) {
    var ctx = this;
    //old line
    //var gen = fn.call(ctx);
    //new line
    var gen = isGenerator(fn) ? fn : fn.call(ctx);
    var it = null;
    function _next(err, res) {
      it = gen.next(res);
      if (it.done) {
        done.call(ctx, err, it.value);
      } else {
        //new line
        it.value = toThunk(it.value,ctx);
        it.value(_next);
      }
    }
    _next();
  }
}

仅仅一个简单的判断,于是世界都清净了,突然就可以yield后面跟generator对象了,就支持深度调用了。虽然有点绕,不过代码真的是太精辟了。

同样的如果我们要支持yield后面跟generatorFunction的话,只需要在toThunk里面再加一个判断:

function isGeneratorFunction(obj) {
  return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
}
function toThunk(obj,ctx){
  if (isGeneratorFunction(obj)) {
    return co(obj.call(ctx));
  }
  if (isGenerator(obj)) {
    return co(obj);
  }
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }
  if (isPromise(obj)) {
    return promiseToThunk.call(ctx, obj);
  }
  return obj;
}

如果是generatorFunction,我们就先执行得到generator再调用co处理。一切就是这么简单。

完整的代码如下:

var fs = require("fs")
function size(file) {
  return function(fn){
    fs.stat(file, function(err, stat){
      if (err) return fn(err);
      fn(null, stat.size);
    });
  }
}
function co(fn) {
  return function(done) {
    var ctx = this;
    //old line
    //var gen = fn.call(ctx);
    //new line
    var gen = isGenerator(fn) ? fn : fn.call(ctx);
    var it = null;
    function _next(err, res) {
      it = gen.next(res);
      if (it.done) {
        done.call(ctx, err, it.value);
      } else {
        //new line
        it.value = toThunk(it.value,ctx);
        it.value(_next);
      }
    }
    _next();
  }
}
function isGeneratorFunction(obj) {
  return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
}
function isGenerator(obj) {
  return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
function isPromise(obj) {
  return obj && 'function' == typeof obj.then;
}
function isObject(obj){
  return obj && Object == obj.constructor;
}
function isArray(obj){
  return Array.isArray(obj);
}
function promiseToThunk(promise){
    return function(done){
        promise.then(function(err,res){
            done(err,res);
        },done)
    }
}
function objectToThunk(obj){
  var ctx = this;
  return function(done){
      var keys = Object.keys(obj);
      var results = new obj.constructor();
      var length = keys.length;
      var _run = function(fn,key){
        fn = toThunk(fn);
        fn.call(ctx,function(err,res){
          results[key] = res;
          --length || done(null, results);
        })
      }
      for(var i in keys){
        _run(obj[keys[i]],keys[i]);
      }

  }
}
function toThunk(obj,ctx){
  if (isGeneratorFunction(obj)) {
    return co(obj.call(ctx));
  }
  if (isGenerator(obj)) {
    return co(obj);
  }
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }
  if (isPromise(obj)) {
    return promiseToThunk.call(ctx, obj);
  }
  return obj;
}


co(function *(){
  var a = size('.gitignore');
  var b = size('package.json');
  var r = yield [a,b];
  return r;
})(function (err,args){
  console.log("callback===args=======");
  console.log(args);

})

这份代码,是去除了co里面很多判断,错误处理之后的代码。用来理解原理更加简单。

结语

什么都不说了,co这样的库。源码不看真的是损失。实在不得不佩服TJ大神的脑子。据说以前还是个搞设计的。有了co,再也不用担心异步回调了。

不过最新的co4.0已经放弃了thunk的风格全部变成了promise。源码也变的更加清晰了。后面会再写一篇分析。

评论
发表评论
2年前
赞了此文章!
2年前

赞哦,不过objectToThunk例子里的_run(Object[keys[i]],keys[i]);是否应是_run(obj[keys[i]],keys[i]);呢?

2年前
赞了此文章!
2年前
赞了此文章!
WRITTEN BY
竹隐
前端。。。
TA的新浪微博
PUBLISHED IN
js从零单排

js从零单排

我的收藏