koa源码分析系列(二)co的实现
更新日期:
koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:
- node v0.11 可以使用 (node —harmony)
- 使用gnode来使用,不过据说性能一般
- 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用,重启chrome即可。
thunk函数
thunk函数是一个偏函数,执行它会得到一个新的只带一个回调参数的函数。下面我们对node的stat举个例子(其实是co官方的例子):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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我们会怎么编程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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做些改进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 | 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
的实现方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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
就是这种思路。
首先我们先解释下面这两句的意思:
1 2 | var keys = Object.keys(obj); var results = new obj.constructor(); |
这么写是为了通用性,Object.keys
接收一个数组或者对象,返回key值。eg:
1 2 | 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函数。这样 我们通过这层转换,使得数组里面的函数可以并行执行。
通过这层封装我们可以这么调用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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函数做下改造:
1 2 3 4 5 6 7 8 | 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的设计真是太强大。
这样 我们就可以这么调用了:
1 2 3 4 5 6 7 | 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里面加点东西
1 2 3 4 5 6 7 8 9 10 11 12 | 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
的实现也比较容易:
1 2 3 4 5 6 7 | 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里面加一个判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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函数做点小改动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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里面再加一个判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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处理。一切就是这么简单。
完整的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 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。。。。