@cherishpeace
2014-05-18T13:22:13.000000Z
字数 7785
阅读 1685
koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。
1. koa源码分析系列(一)generator
2. koa源码分析系列(二)co的实现
3. koa源码分析系列(三)koa的中间件机制实现
koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:
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(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);})//下面是结果,实际的数据根据你的文件会有不同/*121215callback===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实际的规范。
上面我们实现了一个最简单的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 lineit.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 linefn = 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);})()
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 linevar 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 lineit.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 linevar 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 lineit.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。。。。