转载请注明: 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了。