進(jìn)入正文之前,先插播一條消息。 我七年前翻譯的《軟件隨想錄》再版了(京東鏈接)。這次是《Joel論軟件》兩卷同時再版,第一卷是新譯本,第二卷是我翻譯的。 
本書的作者是著名程序員、StackOverflow的創(chuàng)始人 Joel Splosky。我覺得,它是軟件項(xiàng)目管理的最好讀物之一,推薦閱讀。 ======================================== 以下是《深入掌握 ECMAScript 6 異步編程》系列文章的第三篇。 一、什么是 co 函數(shù)庫?co 函數(shù)庫是著名程序員 TJ Holowaychuk 于2013年6月發(fā)布的一個小工具,用于 Generator 函數(shù)的自動執(zhí)行。 
比如,有一個 Generator 函數(shù),用于依次讀取兩個文件。 var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co 函數(shù)庫可以讓你不用編寫 Generator 函數(shù)的執(zhí)行器。 var co = require('co');
co(gen);
上面代碼中,Generator 函數(shù)只要傳入 co 函數(shù),就會自動執(zhí)行。 co 函數(shù)返回一個 Promise 對象,因此可以用 then 方法添加回調(diào)函數(shù)。 co(gen).then(function (){
console.log('Generator 函數(shù)執(zhí)行完成');
})
上面代碼中,等到 Generator 函數(shù)執(zhí)行結(jié)束,就會輸出一行提示。 二、 co 函數(shù)庫的原理為什么 co 可以自動執(zhí)行 Generator 函數(shù)? 前面文章說過,Generator 函數(shù)就是一個異步操作的容器。它的自動執(zhí)行需要一種機(jī)制,當(dāng)異步操作有了結(jié)果,能夠自動交回執(zhí)行權(quán)。 兩種方法可以做到這一點(diǎn)。 (1)回調(diào)函數(shù)。將異步操作包裝成 Thunk 函數(shù),在回調(diào)函數(shù)里面交回執(zhí)行權(quán)。 (2)Promise 對象。將異步操作包裝成 Promise 對象,用 then 方法交回執(zhí)行權(quán)。
co 函數(shù)庫其實(shí)就是將兩種自動執(zhí)行器(Thunk 函數(shù)和 Promise 對象),包裝成一個庫。使用 co 的前提條件是,Generator 函數(shù)的 yield 命令后面,只能是 Thunk 函數(shù)或 Promise 對象。 上一篇文章已經(jīng)介紹了基于 Thunk 函數(shù)的自動執(zhí)行器。下面來看,基于 Promise 對象的自動執(zhí)行器。這是理解 co 函數(shù)庫必須的。 三、基于 Promise 對象的自動執(zhí)行還是沿用上面的例子。首先,把 fs 模塊的 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('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
然后,手動執(zhí)行上面的 Generator 函數(shù)。 var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
})
手動執(zhí)行其實(shí)就是用 then 方法,層層添加回調(diào)函數(shù)。理解了這一點(diǎn),就可以寫出一個自動執(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();
}
run(gen);
上面代碼中,只要 Generator 函數(shù)還沒執(zhí)行到最后一步,next 函數(shù)就調(diào)用自身,以此實(shí)現(xiàn)自動執(zhí)行。 四、co 函數(shù)庫的源碼co 就是上面那個自動執(zhí)行器的擴(kuò)展,它的源碼只有幾十行,非常簡單。 首先,co 函數(shù)接受 Generator 函數(shù)作為參數(shù),返回一個 Promise 對象。 function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的 Promise 對象里面,co 先檢查參數(shù) gen 是否為 Generator 函數(shù)。如果是,就執(zhí)行該函數(shù),得到一個內(nèi)部指針對象;如果不是就返回,并將 Promise 對象的狀態(tài)改為 resolved 。 function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
接著,co 將 Generator 函數(shù)的內(nèi)部指針對象的 next 方法,包裝成 onFulefilled 函數(shù)。這主要是為了能夠捕捉拋出的錯誤。 function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
最后,就是關(guān)鍵的 next 函數(shù),它會反復(fù)調(diào)用自身。 function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
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) + '"'));
}
});
上面代碼中,next 函數(shù)的內(nèi)部代碼,一共只有四行命令。 第一行,檢查當(dāng)前是否為 Generator 函數(shù)的最后一步,如果是就返回。 第二行,確保每一步的返回值,是 Promise 對象。 第三行,使用 then 方法,為返回值加上回調(diào)函數(shù),然后通過 onFulfilled 函數(shù)再次調(diào)用 next 函數(shù)。 第四行,在參數(shù)不符合要求的情況下(參數(shù)非 Thunk 函數(shù)和 Promise 對象),將 Promise 對象的狀態(tài)改為 rejected,從而終止執(zhí)行。
五、并發(fā)的異步操作co 支持并發(fā)的異步操作,即允許某些操作同時進(jìn)行,等到它們?nèi)客瓿桑胚M(jìn)行下一步。 這時,要把并發(fā)的操作都放在數(shù)組或?qū)ο罄锩妗?/p> // 數(shù)組的寫法co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);// 對象的寫法co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
(完)
|