@cherishpeace
2014-05-18T21:22:13.000000Z
字数 7785
阅读 1486
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);
})
//下面是结果,实际的数据根据你的文件会有不同
/*
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实际的规范。
上面我们实现了一个最简单的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);
})()
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。。。。