Co源碼以及與Koa的深入理解
tj大神的co,將本來(lái)應(yīng)該是一個(gè)數(shù)據(jù)類(lèi)型的generator變成了一種處理異步的解決方案
其實(shí)主要就是一個(gè)遍歷函數(shù),將promise或者generator的異步函數(shù)一直執(zhí)行到得到最后的結(jié)果再返回,這樣就可以把本來(lái)放到異步中的方法按照同步的順序來(lái)寫(xiě)。
yield
函數(shù)內(nèi)部的yield后面?zhèn)魅氲目梢允且韵?/p>
- Promise(就是promise嘛)
- thunks(就是一個(gè)偏函數(shù),執(zhí)行之后只有一個(gè)簡(jiǎn)單的擁有一個(gè)callback的參數(shù)的函數(shù))
- array(通過(guò)array可以并行執(zhí)行里面的function,
并行是主要的價(jià)值) - objects(和array相同,也是并行執(zhí)行里面的yieldable,
并行是主要的價(jià)值) - generators
- generators functions(下面的這兩個(gè)東西可以支持,但是并不被推薦,因?yàn)槲覀儜?yīng)該轉(zhuǎn)向更加標(biāo)準(zhǔn)的promise)
API
co(fn*).then
將一個(gè)generator解決為一個(gè)promise
var fn = co.wrap(fn*)
講一個(gè)generator轉(zhuǎn)化為一個(gè)返回promise的常規(guī)函數(shù)
本質(zhì)的探索
他的最初實(shí)現(xiàn)是基于Thunk函數(shù)的。接收了一個(gè)生成器函數(shù)作為參數(shù),并生成了一個(gè)實(shí)際操作函數(shù),函數(shù)通過(guò)接收回調(diào)的方式來(lái)傳入最后的返回值。
所以先了解下thunk函數(shù)
這東西的發(fā)展是由函數(shù)的求值策略的分歧決定的,兩種求值策略
var b = 1;
function a(x,y){
return y;
}
a(b+1);
上面的代碼一b+1在什么時(shí)候執(zhí)行比較好,
一種是傳值調(diào)用,在進(jìn)入函數(shù)體之前就直接執(zhí)行完,把值傳進(jìn)去。c語(yǔ)言是這么做的
一種是傳名調(diào)用,將表達(dá)式傳入函數(shù)體,只在用到他的時(shí)候求值。Hskell語(yǔ)言是這么做的
前一種會(huì)簡(jiǎn)單一些,但是會(huì)有性能損失,所以傾向于傳名調(diào)用。
傳名函數(shù)的編譯器實(shí)現(xiàn),其實(shí)就是放入一個(gè)臨時(shí)函數(shù),再將臨時(shí)函數(shù)傳入函數(shù)體,這個(gè)臨時(shí)函數(shù)就叫做thunk函數(shù)。
js語(yǔ)言是傳值調(diào)用,他的thunk含義有些不同,js中,thunk函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù),將它替換成單參數(shù)的版本,且只接受回調(diào)函數(shù)作為參數(shù)。
//正常的readFile函數(shù)
fs.readFile(fileName, callback);
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
//thunk版本的函數(shù)
function Thunk(fileName){
return function(callback){
fs.readFile(fileName,callback);
}
}
所以其實(shí)任何有回調(diào)的函數(shù)都是可以搞成thunk形式的,下面是一個(gè)簡(jiǎn)單的生成器
var Thunk = function(fn){
return function () {
//先傳入其他的參數(shù)初始化
var args = Array.prototype.slice.call(arguments);
//傳入callback返回的函數(shù)
return function(callback){
args.push(callback);
//實(shí)際調(diào)用的時(shí)候
return fn.apply(this,args);
}
}
}
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
tj的thunkify源碼
/**
* Module dependencies.
*/
var assert = require('assert');
/**
* Expose `thunkify()`.
*/
module.exports = thunkify;
/**
* Wrap a regular callback `fn` as a thunk.
*
* @param {Function} fn
* @return {Function}
* @api public
*/
function thunkify(fn){
assert('function' == typeof fn, 'function required');
return function(){
//這里就是將所有的參數(shù)放進(jìn)了一個(gè)新的數(shù)組,這里之所以不用[].slice。是因?yàn)橛腥嗽赽luebird docs發(fā)現(xiàn),如果直接這樣泄露arguments,v8的一些優(yōu)化的編譯會(huì)被擱置,就會(huì)有性能上的損失。
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
//這里用called是為了標(biāo)記只執(zhí)行了一次,類(lèi)似于promise的resolve和reject只能執(zhí)行一次一樣。
var called;
args.push(function(){
if (called) return;
called = true;
//因?yàn)閍rguments是一個(gè)list,必須得用apply才能在done傳入。
done.apply(null, arguments);
});
//這里用個(gè)try catch,可以在執(zhí)行失敗時(shí)走一遍callback,傳入err信息
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};


generator函數(shù)的回調(diào)流程管理
包裝成這樣到底有個(gè)啥用場(chǎng)?用在了generator的流程管理
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFile('/etc/shells');
console.log(r2.toString());
};
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
if (err) throw err;
var r2 = g.next(data);
r2.value(function(err, data){
if (err) throw err;
g.next(data);
});
});
就如同上面的,generator的執(zhí)行過(guò)程實(shí)際上是將同一個(gè)回調(diào)函數(shù),反復(fù)傳入next的value結(jié)果中。這樣我們就可以遞歸的來(lái)自動(dòng)完成這個(gè)過(guò)程了。于是據(jù)誕生了基于thunk函數(shù)的執(zhí)行器,也就是co了。
最簡(jiǎn)單的co
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(gen);
執(zhí)行器幫我們不停地調(diào)用傳入生成器的next函數(shù),如果done為true的時(shí)候,代表迭代完成,會(huì)將值傳給回調(diào)函數(shù)。
當(dāng)然前提是每一個(gè)一步函數(shù)都得是thunk函數(shù)的形式。
thunk并不是generator函數(shù)的自動(dòng)執(zhí)行的唯一方案。我們需要的其實(shí)只是一個(gè)機(jī)制,循環(huán)調(diào)用,并且交出和返回程序的執(zhí)行權(quán),thunk可以做到,promise也可以做到。
首先將readfile包裝成promise形式
var fs =require('fs');
var readFile = function(fileName){
return new Promise(function(resolve,reject){
fs.readFile(fileName,function(error,data){
if(error){reject(error)}
resolve(data);
})
});
}
var gen = function* (){
var f1 = yield readFile('f1.js');
var f2 = yield readFile('f2.js');
console.log(f1);
console.log(f2);
}
然后手動(dòng)執(zhí)行下generator函數(shù)
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
})
寫(xiě)一個(gè)自動(dòng)執(zhí)行器
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if(result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
co的源碼
下面的是co源碼的逐行閱讀,先把參照的一些圖片列舉出來(lái)
//array原生的slice
var slice = Array.prototype.slice;
//這里寫(xiě)的這么古怪就只是想在es6的模塊引入時(shí)更加舒服一些,參見(jiàn)下面的圖片3
module.exports = co['default'] = co.co = co;
//將傳入的generator函數(shù)包裝成一個(gè)返回promise的方法
//這是一個(gè)獨(dú)立的方法,就是將傳入的函數(shù)包裝成了co執(zhí)行前的形式
co.wrap = function (fn) {
//存了一個(gè)指針指向原generator函數(shù)
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
//返回的方法調(diào)用就會(huì)直接執(zhí)行co。
return co.call(this, fn.apply(this, arguments));
}
};
//執(zhí)行g(shù)enerator或者generator函數(shù)然后返回一個(gè)promise
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
// 將所有的東西放到一個(gè)promise里面,來(lái)防止引起內(nèi)存泄露錯(cuò)誤的promise chaining。
//tudo:看一下這個(gè)issue see https://github.com/tj/co/issues/180
//參見(jiàn)下面的內(nèi)存泄露的研究
//https://github.com/promises-aplus/promises-spec/issues/179 看的我好累,完全沒(méi)有看懂啊?。?!
//總之不管怎樣,他是把傳進(jìn)來(lái)的東西包裝成了一個(gè)promise
return new Promise(function(resolve, reject) {
//這里是判斷下gen是不是函數(shù),generators function執(zhí)行之后是一個(gè)object
if (typeof gen === 'function') gen = gen.apply(ctx, args);
//傳入的不是generators函數(shù),沒(méi)有next,就直接resolve返回結(jié)果;這里是錯(cuò)誤兼容而已,因?yàn)閏o就是基于generator的,傳入其他的沒(méi)有意義
if (!gen || typeof gen.next !== 'function') return resolve(gen);
//主要就是走下面的onFulfilled方法,這個(gè)方法返回的是一個(gè)promise(resolve或者reject)
onFulfilled();
function onFulfilled(res) {
var ret;
try {
//調(diào)用第一次next方法
ret = gen.next(res);
} catch (e) {
//出錯(cuò)了直接reject出去
return reject(e);
}
//將第一次的結(jié)果({done:true,value:{}})傳入內(nèi)部方法next
next(ret);
}
//promise失敗的時(shí)候調(diào)用
//這里在promise錯(cuò)誤的時(shí)候,就會(huì)嘗試向外throw err。Genertor的屬性,可以內(nèi)部拋出,外部不活。如果我們對(duì)這個(gè)yield進(jìn)行了try catch,就會(huì)被捕獲,不處理的話,就會(huì)reject出去,在co的catch語(yǔ)句中co(*fn).catch處理。
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
//循環(huán)得到next的結(jié)果,return的還是一個(gè)promise
function next(ret) {
//如果done為true的話,代表執(zhí)行結(jié)束,返回一個(gè)resolve的promise
if (ret.done) return resolve(ret.value);
//既然還沒(méi)執(zhí)行完,就將ret.value轉(zhuǎn)換成一個(gè)promise
var value = toPromise.call(ctx, ret.value);
//如果成功轉(zhuǎn)化為了promise,就在這個(gè)promise執(zhí)行完了再調(diào)用onFulfilled方法
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
//將yield后面的東西轉(zhuǎn)化成一個(gè)promise
function toPromise(obj) {
//如果不存在的話,直接返回,走最后的報(bào)錯(cuò)流程
if (!obj) return obj;
//判斷傳入的是不是promise,是的話直接返回
if (isPromise(obj)) return obj;
//判斷傳入的是不是generator,或者generator function,是的話,繼續(xù)調(diào)用co函數(shù)進(jìn)行循環(huán)~
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
//如果就是個(gè)普通的thunk函數(shù),也把他轉(zhuǎn)化為promise
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
//如果是array或者object的話,也走相應(yīng)地變換方法
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
//如果都不是,直接返回,走最后的報(bào)錯(cuò)流程
return obj;
}
//這里將thunk轉(zhuǎn)化成了promise,thunk就是調(diào)用的時(shí)候傳入一個(gè)error和res的function,就在最外面包了個(gè)promise就行了
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
//這里的array轉(zhuǎn)化為promise其實(shí)就是通過(guò)Promise.all來(lái)包裹,這個(gè)方法只接受promise的數(shù)組,并且裝化為一個(gè)新的promise
//參見(jiàn)下面的promise平行執(zhí)行的研究
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
//將一個(gè)object轉(zhuǎn)化為promise,其實(shí)就是內(nèi)部調(diào)用了promise.all方法而已
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
//檢查是否是promise,果然就是簡(jiǎn)單的判斷他有沒(méi)有then方法
function isPromise(obj) {
return 'function' == typeof obj.then;
}
//這里判斷是不是generator就是判斷他的next和throw方法是不是function
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
//判斷是否是generatorFunction就是判斷了他的constructor的name
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
//這里是為了解決沒(méi)有constructor的對(duì)象,比如Object.create(null)
if (!constructor) return false;
//這里兩種情況會(huì)返回true,一種是名字正確地,一種是他的prototype是generator
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
//就是通過(guò)constructor來(lái)判斷是不是一個(gè)簡(jiǎn)單的對(duì)象
function isObject(val) {
return Object == val.constructor;
}
圖片3

promise chaining導(dǎo)致的內(nèi)存泄露
這里只是源碼的一個(gè)小注釋?zhuān)タ戳瞬簧贃|西
閱讀了https://github.com/tj/co/issues/180
有人發(fā)現(xiàn)在一個(gè)無(wú)限循環(huán)的for循環(huán)里面使用co調(diào)用一個(gè)異步操作,會(huì)發(fā)生內(nèi)存泄露
有人推斷是所有的promise都被連接了起來(lái),阻止了gc的回收
有人測(cè)試了基于thunk的v3,發(fā)現(xiàn)ok,v4發(fā)現(xiàn)內(nèi)存泄露,并且使用工具發(fā)現(xiàn)確實(shí)是promise的問(wèn)題
死馬說(shuō)這事規(guī)范里的問(wèn)題,bluebird和then/promise已經(jīng)做出了修復(fù),
最后hax說(shuō)是es6 spec “bug”
接下來(lái)看一個(gè)解決方案
就是用一個(gè)promise從外面包裹住全部,到底為啥還是沒(méi)看懂....跪求大神賜教
promise的平行執(zhí)行
promise被創(chuàng)建的時(shí)候就開(kāi)始了他們的任務(wù),是無(wú)法被執(zhí)行的。他們只代表了結(jié)果的狀態(tài),將他們傳給promise.all的時(shí)候甚至都是并行執(zhí)行的。他不關(guān)心執(zhí)行順序,也不關(guān)心是否平行運(yùn)行。
Co的錯(cuò)誤處理
這里主要是涉及到generator.throw方法,可以在generator外部拋出異常,而在generator內(nèi)部來(lái)catch住異常。
co里面使用了這個(gè)屬性,就可以針對(duì)某幾個(gè)yield進(jìn)行try catch,如果不進(jìn)行處理,統(tǒng)一的會(huì)在后面的catch語(yǔ)句中co(*fn).catch找到。
Koa執(zhí)行的理解

請(qǐng)求進(jìn)來(lái)的時(shí)候會(huì)一次經(jīng)過(guò)各個(gè)中間件進(jìn)行執(zhí)行,中間件之間的跳轉(zhuǎn)是yield next,執(zhí)行完了之后就會(huì)逆序執(zhí)行。
app.use(function *(next){
var start = new Date;
//執(zhí)行到這句話的時(shí)候跳到下一個(gè)中間件
yield next;
//下面的中間件執(zhí)行完了之后再執(zhí)行下面的部分
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
寫(xiě)兩個(gè)yield next會(huì)有什么問(wèn)題?
這里遇到y(tǒng)ield next其實(shí)還是會(huì)繼續(xù)向下執(zhí)行下一個(gè)generator的,但是因?yàn)橄乱粋€(gè)中間件done的狀態(tài)已經(jīng)是true了,再次調(diào)用一下此前已經(jīng)執(zhí)行完的generator,調(diào)用返回的結(jié)果肯定還是done為true,因?yàn)榇饲耙呀?jīng)執(zhí)行完了。所以后面繼續(xù)yield next是沒(méi)有意義的~~
Koa的中間件是運(yùn)行在co函數(shù)之下的。
Koa的中間件的實(shí)現(xiàn)
這里看到了一個(gè)Koa中間件的實(shí)現(xiàn)
var gens = [];
function use(generetor){
gens.push(generetor);
}
// 實(shí)現(xiàn)co函數(shù)
function co(flow, isGenerator){
var gen;
if (isGenerator) {
gen = flow;
} else {
gen = flow();
}
return new Promise(function(resolve){
var next = function(data){
var result = gen.next(data);
var value = result.value;
// 如果調(diào)用完畢,調(diào)用resolve
if(result.done){
resolve(value);
return;
}
// 如果為yield后面接的為generator,傳入co進(jìn)行遞歸,并且將promise返回
if (typeof value.next === "function" && typeof value.throw === "function") {
value = co(value, true);
}
if(value.then){
// 當(dāng)promise執(zhí)行完畢,調(diào)用next處理下一個(gè)yield
value.then(function(data){
next(data);
})
}
};
next();
});
}
function trigger(){
var prev = null;
var m = gens.length;
co(function*(){
while(m--){
// 形成鏈?zhǔn)絞enerator
prev = gens[m].call(null, prev);
}
// 執(zhí)行最外層generator方法
yield prev;
})
}
use(function*(next){
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step1")
}, 1000)
});
console.log(d);
yield next;
console.log("step2");
});
use(function*(next){
console.log("step3");
yield next;
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step4")
}, 1000)
});
console.log(d);
});
use(function*(){
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step5")
}, 1000)
});
console.log(d);
console.log("step6");
});
trigger();
Koa的運(yùn)行順序圖

這張圖非常詳細(xì)了,原來(lái)本身的respond,以及自己定義的一些中間件統(tǒng)一的會(huì)被整成一個(gè)generator,然后交給co來(lái)執(zhí)行。
本文看了不少文章,引用的一些列出了原地址:
http://segmentfault.com/a/1190000002783230
http://www.ruanyifeng.com/blog/2015/05/thunk.html
http://www.cnblogs.com/axes/p/4683176.html
http://purplebamboo.github.io/2015/01/16/koa-source-analytics-4/
先寫(xiě)到這里,順便給個(gè)github的傳送門(mén),喜歡的朋友star一下啊,自己平時(shí)遇到的問(wèn)題以及一下學(xué)習(xí)的經(jīng)歷及所得都會(huì)在github上進(jìn)行記錄~