[关闭]
@cherishpeace 2014-05-18T13:22:13.000000Z 字数 7785 阅读 1356

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

koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
1. koa源码分析系列(一)generator
2. koa源码分析系列(二)co的实现
3. koa源码分析系列(三)koa的中间件机制实现

环境准备

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

thunk函数

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

  1. var fs = require('fs');
  2. function size(file) {
  3. return function(fn){
  4. fs.stat(file, function(err, stat){
  5. if (err) return fn(err);
  6. fn(null, stat.size);
  7. });
  8. }
  9. }
  10. var getIndexSize = size("./index.js");
  11. getIndexSize(function(size){
  12. console.log(size);
  13. })

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

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

最简单的co实现

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

  1. co(function *(){
  2. var a = yield size('.gitignore');
  3. var b = yield size('package.json');
  4. console.log(a);
  5. console.log(b);
  6. return [a,b];
  7. })(function (err,args){
  8. console.log("callback===args=======");
  9. console.log(args);
  10. })
  11. //下面是结果,实际的数据根据你的文件会有不同
  12. /*
  13. 12
  14. 1215
  15. callback===args=======
  16. [ 12, 1215 ]
  17. */

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

  1. function co(fn) {
  2. return function(done) {
  3. var ctx = this;
  4. var gen = fn.call(ctx);
  5. var it = null;
  6. function _next(err, res) {
  7. it = gen.next(res);
  8. if (it.done) {
  9. done.call(ctx, err, it.value);
  10. } else {
  11. it.value(_next);
  12. }
  13. }
  14. _next();
  15. }
  16. }

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做些改进:

  1. function co(fn) {
  2. return function(done) {
  3. var ctx = this;
  4. var gen = fn.call(ctx);
  5. var it = null;
  6. function _next(err, res) {
  7. it = gen.next(res);
  8. if (it.done) {
  9. done.call(ctx, err, it.value);
  10. } else {
  11. //new line
  12. it.value = toThunk(it.value,ctx);
  13. it.value(_next);
  14. }
  15. }
  16. _next();
  17. }
  18. }

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

  1. function isObject(obj){
  2. return obj && Object == obj.constructor;
  3. }
  4. function isArray(obj){
  5. return Array.isArray(obj);
  6. }
  7. function toThunk(obj,ctx){
  8. if (isObject(obj) || isArray(obj)) {
  9. return objectToThunk.call(ctx, obj);
  10. }
  11. return obj;
  12. }

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

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

  1. function objectToThunk(obj){
  2. var ctx = this;
  3. return function(done){
  4. var keys = Object.keys(obj);
  5. var results = new obj.constructor();
  6. var length = keys.length;
  7. var _run = function(fn,key){
  8. fn.call(ctx,function(err,res){
  9. results[key] = res;
  10. --length || done(null, results);
  11. })
  12. }
  13. foreach(var i in keys){
  14. _run(Object[keys[i]],keys[i]);
  15. }
  16. }
  17. }

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

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

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

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

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

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

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

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

  1. co(function *(){
  2. var a = size('.gitignore');
  3. var b = size('package.json');
  4. var r = yield [a,b];
  5. return r;
  6. })(function (err,args){
  7. console.log("callback===args=======");
  8. console.log(args);
  9. })
  10. /*
  11. callback===args=======
  12. [ 12, 1215 ]
  13. */

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

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

  1. var _run = function(fn,key){
  2. //new line
  3. fn = toThunk(fn);
  4. fn.call(ctx,function(err,res){
  5. results[key] = res;
  6. --length || done(null, results);
  7. })
  8. }

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

  1. co(function *(){
  2. var a = [size('.gitignore'), size('index.js')];
  3. var b = [size('.gitignore'), size('index.js')];
  4. var c = [size('.gitignore'), size('index.js')];
  5. var d = yield [a, b, c];
  6. console.log(d);
  7. })()

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

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

  1. function isPromise(obj) {
  2. return obj && 'function' == typeof obj.then;
  3. }
  4. function toThunk(obj,ctx){
  5. if (isObject(obj) || isArray(obj)) {
  6. return objectToThunk.call(ctx, obj);
  7. }
  8. if (isPromise(obj)) {
  9. return promiseToThunk.call(ctx, obj);
  10. }
  11. return obj;
  12. }

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

  1. function promiseToThunk(promise){
  2. return function(done){
  3. promise.then(function(err,res){
  4. done(err,res);
  5. },done)
  6. }
  7. }

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

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

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

  1. function isGenerator(obj) {
  2. return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
  3. }
  4. function toThunk(obj,ctx){
  5. if (isGenerator(obj)) {
  6. return co(obj);
  7. }
  8. if (isObject(obj) || isArray(obj)) {
  9. return objectToThunk.call(ctx, obj);
  10. }
  11. if (isPromise(obj)) {
  12. return promiseToThunk.call(ctx, obj);
  13. }
  14. return obj;
  15. }

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

  1. function co(fn) {
  2. return function(done) {
  3. var ctx = this;
  4. //old line
  5. //var gen = fn.call(ctx);
  6. //new line
  7. var gen = isGenerator(fn) ? fn : fn.call(ctx);
  8. var it = null;
  9. function _next(err, res) {
  10. it = gen.next(res);
  11. if (it.done) {
  12. done.call(ctx, err, it.value);
  13. } else {
  14. //new line
  15. it.value = toThunk(it.value,ctx);
  16. it.value(_next);
  17. }
  18. }
  19. _next();
  20. }
  21. }

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

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

  1. function isGeneratorFunction(obj) {
  2. return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
  3. }
  4. function toThunk(obj,ctx){
  5. if (isGeneratorFunction(obj)) {
  6. return co(obj.call(ctx));
  7. }
  8. if (isGenerator(obj)) {
  9. return co(obj);
  10. }
  11. if (isObject(obj) || isArray(obj)) {
  12. return objectToThunk.call(ctx, obj);
  13. }
  14. if (isPromise(obj)) {
  15. return promiseToThunk.call(ctx, obj);
  16. }
  17. return obj;
  18. }

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

完整的代码如下:

  1. var fs = require("fs")
  2. function size(file) {
  3. return function(fn){
  4. fs.stat(file, function(err, stat){
  5. if (err) return fn(err);
  6. fn(null, stat.size);
  7. });
  8. }
  9. }
  10. function co(fn) {
  11. return function(done) {
  12. var ctx = this;
  13. //old line
  14. //var gen = fn.call(ctx);
  15. //new line
  16. var gen = isGenerator(fn) ? fn : fn.call(ctx);
  17. var it = null;
  18. function _next(err, res) {
  19. it = gen.next(res);
  20. if (it.done) {
  21. done.call(ctx, err, it.value);
  22. } else {
  23. //new line
  24. it.value = toThunk(it.value,ctx);
  25. it.value(_next);
  26. }
  27. }
  28. _next();
  29. }
  30. }
  31. function isGeneratorFunction(obj) {
  32. return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
  33. }
  34. function isGenerator(obj) {
  35. return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
  36. }
  37. function isPromise(obj) {
  38. return obj && 'function' == typeof obj.then;
  39. }
  40. function isObject(obj){
  41. return obj && Object == obj.constructor;
  42. }
  43. function isArray(obj){
  44. return Array.isArray(obj);
  45. }
  46. function promiseToThunk(promise){
  47. return function(done){
  48. promise.then(function(err,res){
  49. done(err,res);
  50. },done)
  51. }
  52. }
  53. function objectToThunk(obj){
  54. var ctx = this;
  55. return function(done){
  56. var keys = Object.keys(obj);
  57. var results = new obj.constructor();
  58. var length = keys.length;
  59. var _run = function(fn,key){
  60. fn = toThunk(fn);
  61. fn.call(ctx,function(err,res){
  62. results[key] = res;
  63. --length || done(null, results);
  64. })
  65. }
  66. for(var i in keys){
  67. _run(obj[keys[i]],keys[i]);
  68. }
  69. }
  70. }
  71. function toThunk(obj,ctx){
  72. if (isGeneratorFunction(obj)) {
  73. return co(obj.call(ctx));
  74. }
  75. if (isGenerator(obj)) {
  76. return co(obj);
  77. }
  78. if (isObject(obj) || isArray(obj)) {
  79. return objectToThunk.call(ctx, obj);
  80. }
  81. if (isPromise(obj)) {
  82. return promiseToThunk.call(ctx, obj);
  83. }
  84. return obj;
  85. }
  86. co(function *(){
  87. var a = size('.gitignore');
  88. var b = size('package.json');
  89. var r = yield [a,b];
  90. return r;
  91. })(function (err,args){
  92. console.log("callback===args=======");
  93. console.log(args);
  94. })

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

结语

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注