node js co分析2

转载请注明: TheViper http://www.cnblogs.com/TheViper 

更好的可以看http://purplebamboo.github.io/2014/05/24/koa-source-analytics-2/

源码

function co(fn) {
  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) {
      var args = slice.call(arguments), len = args.length;
      var hasCallback = len && 'function' == typeof args[len - 1];
      done = hasCallback ? args.pop() : error;
      gen = fn.apply(this, args);
    } else {
      done = done || error;
    }

    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;

      // ok
      if (!err) {
        try {
          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.call(ctx, function(){
            if (called) return;
            called = true;
            next.apply(ctx, arguments);
          });
        } catch (e) {
          setImmediate(function(){
            if (called) return;
            called = true;
            next(e);
          });
        }
        return;
      }
    }
  }
}

var isGenFun = isGeneratorFunction(fn);,判断是不是generator function,其实这个从例子看来基本上都是。然后判断有没有回调。即是不是有

co(function *(){
  var results = yield *foo();
  console.log(results);
  return results;
})(function(err,res){
    console.log('res')
});

里面的function(err,res){...},如果有,将其赋值给done.然后是gen = fn.apply(this, args);,也就是上一篇里面说generator时候的var it = start();,start是一个generator function,这时,函数还没有执行。

然后是next();,ret = gen.next(res);,开始运行*foo()里面的第一个yield size(),返回形如{value:[function],done:false}的对象。

function *foo(){
  var a = yield size('node_modules/thunkify/.npmignore');
  var b = yield size('node_modules/thunkify/Makefile');
  var c = yield size('node_modules/thunkify/package.json');
  return [a, b, c];
}

然后ret.done判断是不是完成了。如果完成了,exit()执行回调.可以看到这里向回调传入了两个参数,err是错误信息,res是yield执行返回的结果。

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

如果没有完成,ret.value = toThunk(ret.value, ctx);,对yield执行返回的结果格式化一下,

function toThunk(obj, ctx) {
  if (isGeneratorFunction(obj)) {
    return co(obj.call(ctx));
  }

  if (isGenerator(obj)) {
    return co(obj);
  }

  if (isPromise(obj)) {
    return promiseToThunk(obj);
  }

  if ('function' == typeof obj) {
    return obj;
  }

  if (isObject(obj) || Array.isArray(obj)) {
    return objectToThunk.call(ctx, obj);
  }

  return obj;
}

如果ret.value是generator,继续co(fn),如果是promise,返回thunk形式

function promiseToThunk(promise) {
  return function(fn){
    promise.then(function(res) {
      fn(null, res);
    }, fn);
  }
}

如果是函数,返回函数。

如果是object或array,稍微复杂点,这个最后说。

然后如果ret.value是函数,执行这个函数并传入一个回调函数,

function(){
     if (called) return;
     called = true;
     next.apply(ctx, arguments);
}

这也就是为什么给yield传入的函数要写出形如

function size(file) {
  return function(fn){
    fs.stat(file, function(err, stat){
      if (err) return fn(err);
      fn(null,stat.size);
    });
  }
}

当这个异步操作执行后,会向fn传入两个参数,因为function next(err, res)。

上一篇里面有一个例子将回调变为一个参数,也就是fn(stat.size)取代fn(null,stat.size),只会返回第一个yield结果,因为这里没有参数移位,只通过参数的位置判断传入的参数所对应的形参,所以stat.size就被认为是err了,也就是直接exit()了,所以定义thunk的时候一定要传入两个参数,而且位置不能变。

顺便说一下上一篇里面提到的thunkify,它是用来将一般异步操作变成thunk形式,源码很简单

function thunkify(fn){
  assert('function' == typeof fn, 'function required');

  return function(){
    var args = new Array(arguments.length);
    var ctx = this;

    for(var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function(done){
      var called;

      args.push(function(){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });
      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

使用

var size=thunkify(fs.stat);
function *foo(){
  var a = yield size('node_modules/thunkify/.npmignore');
  var b = yield size('node_modules/thunkify/Makefile');
  var c = yield size('node_modules/thunkify/package.json');
  return [a, b, c];
}

co(function *(){
  var results = yield *foo();
  console.log(results);
  return results;
})();

思想就是把参数添加到args数组,最后再向args数组添加

function(){
        if (called) return;
        called = true;
        done.apply(null, arguments);
 }

这个回调,作为fn.apply(ctx, args);执行后的回调。里面的done是上面co源码里面的

function(){
   if (called) return;
   called = true;
   next.apply(ctx, arguments);
}

done这里就相当于自己写的thunk里面的fn.

thunk的总结下就是在异步操作外面封装一个return function(fn),fn是co向thunk传入的回调,里面有next().然后在异步操作回调里面要触发fn,以保证fn的next()会执行。

回到主线,然后next.apply(ctx,arguments);,执行下一个yield.

最后说一下toThunk()里面的objectToThunk

function objectToThunk(obj){
  var ctx = this;
  var isArray = Array.isArray(obj);

  return function(done){
    var keys = Object.keys(obj);
    var pending = keys.length;
    var results = isArray
      ? new Array(pending) // predefine the array length
      : new obj.constructor();
    var finished;// prepopulate object keys to preserve key ordering
    if (!isArray) {
      for (var i = 0; i < pending; i++) {
        results[keys[i]] = undefined;
      }
    }

    for (var i = 0; i < keys.length; i++) {
      run(obj[keys[i]], keys[i]);
    }

    function run(fn, key) {
      if (finished) return;
      try {
        fn = toThunk(fn, ctx);

        if ('function' != typeof fn) {
          results[key] = fn;
          return --pending || done(null, results);
        }

        fn.call(ctx, function(err, res){
          if (finished) return;

          if (err) {
            finished = true;
            return done(err);
          }

          results[key] = res;
          --pending || done(null, results);
        });
      } catch (err) {
        finished = true;
        done(err);
      }
    }
  }
}

例子

function *foo(){
  var a= yield {
    name: {
      first: yield size('node_modules/thunkify/.npmignore'),
      last:yield size('node_modules/thunkify/Makefile')
    }
  };
  return a;//{name:{first:13,last:39}
}

Object.keys(obj)将oject的所有key添加到一个数组。这里是是['name'],['first','last'],然后初始化最终返回的results.接着就是run()

    for (var i = 0; i < keys.length; i++) {
      run(obj[keys[i]], keys[i]);
    }

例子里面有一层嵌套,所以obj[keys[i]]还是对象,这没关系,后面会处理。

注意到fn = toThunk(fn, ctx);,如果fn是对象的话,又会去调用objectToThunk(obj)。....如果fn中有嵌套,toThunk(fn,ctx)会进行深度遍历。

如果返回的fn不是function,则认为确实是嵌套到了尽头,也就是最终函数。--pending判断key是不是key数组的最后一个了,如果是就done(null, results)

如果fn是function,就和上面的next()差不多了,不同的是各个函数执行一次就对公用的长度变量减一,不需要关心各个函数的执行顺序,只要当其中一个函数发现变量变为0时,代表其他函数都执行好了,是最后一个,于是就可以调用回调函数done了。

 

posted on 2015-01-11 00:24  TheViper_  阅读(702)  评论(0编辑  收藏  举报