Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa源码分析系列(二)co的实现 #8

Open
purplebamboo opened this issue Jul 11, 2017 · 0 comments
Open

koa源码分析系列(二)co的实现 #8

purplebamboo opened this issue Jul 11, 2017 · 0 comments

Comments

@purplebamboo
Copy link
Owner

koa源码分析系列(二)co的实现

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,再也不用担心异步回调了。妈妈再也不用担心“恶魔金字塔了”so happy。。。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant