generator函數可以理解成一個異步操作的容器,它裝着一些異步操作,但並不會在實例化以後立即執行。而co的思想是在恰當的時候執行這些異步操作。那麼就需要一種機制,在一個異步操作執行完畢以後通知下一個異步操作開始執行。額,這句話聽起來就有點耳熟了。這不就是回調函數或者promise乾的事麼。確實,co要求generator裏yield的是thunk或者promise就是這個道理。thunk就是一種回調機制。
那麼co就有兩種實現方式,promise或者thunk。co4.0之前是用thunk實現的,之後是用promise實現的。
以下所有代碼可以在github上查看源碼。
基於thunk實現co
查看thunk函數。
大致思路就是,在yield中調用thunk,thunk回調中調用genetator的next方法,直至next返回的對象{value, done}中done爲true。
function run(fn) {
//實例化generator,但因爲generator的特性,並沒有調用
var gen = fn();
function next(err, data) {
var result = gen.next(data);
//在調用下一次next之前,你可以加一些自己的操作
console.log('--------next in thunk----------');
if (result.done) return;
//{value:next傳入的值,done:boolean},由於fn應爲一個yield thunk的generator,故value接受的其實是一個回調,這個回調在co(或者說本函數run中就是generator的next)
result.value(next);
}
next();
}
調用
var readFile = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFile('./hi.txt');
console.log(r1.toString());
var r2 = yield readFile('./hello.txt');
console.log(r2.toString());
};
console.log('------------------run thunk-----------------');
run(gen);
執行結果:
thunkify是如何實現的請移步github查看,你也可以使用require的方式,nodejs有這個工具。
基於promise實現co
大致思路同thunk一樣,只是把回調的實現方式變成了promise,同理就需要genenrator中yield的是promise。
function run2(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();
}
調用方式:
var readFile2 = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen2 = function* (){
var f1 = yield readFile2('./hi.txt');
console.log(f1.toString());
var f2 = yield readFile2('./hello.txt');
console.log(f2.toString());
};
console.log('-----------------run2 promise-----------------');
run2(gen2);
執行結果:
co源碼分析
基本原理就是以上所說。co就是對以上兩種方式進行擴展、封裝。
function co(gen) {
var ctx = this;
//如果是generatorFunction,就執行 獲得對應的generator對象
if (typeof gen === 'function') gen = gen.call(this);
//返回一個promise
return new Promise(function(resolve, reject) {
//初始化入口函數,第一次調用
onFulfilled();
//成功狀態下的回調
function onFulfilled(res) {
var ret;
try {
//拿到第一個yield返回的對象值ret
ret = gen.next(res);
} catch (e) {
//出錯直接調用reject把promise置爲失敗狀態
return reject(e);
}
//開啓調用鏈
next(ret);
}
function onRejected(err) {
var ret;
try {
//拋出錯誤,這邊使用generator對象throw。這個的好處是可以在co的generatorFunction裏面使用try捕獲到這個異常。
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
//如果執行完成,直接調用resolve把promise置爲成功狀態
if (ret.done) return resolve(ret.value);
//把yield的值轉換成promise
//支持 promise,generator,generatorFunction,array,object
//toPromise的實現可以先不管,只要知道是轉換成promise就行了
var value = toPromise.call(ctx, ret.value);
//成功轉換就可以直接給新的promise添加onFulfilled, onRejected。當新的promise狀態變成結束態(成功或失敗)。就會調用對應的回調。整個next鏈路就執行下去了。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//否則說明有錯誤,調用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) + '"'));
}
});
}
function isPromise(obj) {
return 'function' == typeof obj.then;
}
在co4.0中,你依然可以yield thunk,因爲co會將其轉換爲promise,你可以yield object或者array,co也會將其轉換爲promise,且object或array的成員會同步開始執行。