異步編程系列教程:
- (翻譯)異步編程之Promise(1)——初見魅力
- 異步編程之Promise(2):探究原理
- 異步編程之Promise(3):拓展進階
- 異步編程之Generator(1)——領略魅力
- 異步編程之Generator(2)——剖析特性
- 異步編程之co——源碼分析
如何使用co
大家如果能消化掉前面的知識,相信這一章的分析也肯定是輕輕松松的。我們這一章就來說說,我們之前一直高調提到的co
庫。co
庫,它用Generator和Promise相結合,完美提升了我們異步編程的體驗。我們首先看看如何使用co
的,我們仍舊以之前的讀取Json文件的例子看看:
// 注意readFile已經是Promise化的異步API
co(function* (){
var filename = yield readFile('hello3.txt', 'utf-8');
var json = yield readFile(filename, 'utf-8');
return JSON.parse(json).message;
}).then(console.log, console.error);
大家看上面的代碼,甚至是可以使用同步的思維,不用去理會回調什么鬼的。我們readFile()
得到filename
,然后再次readFile()
得到json
,解析完json后輸出就結束了,非常清爽。大家如果不相信的話,可以使用原生的異步api嘗試一下,fs.readFile()
像上面相互有依賴的,絕對惡心!
我們可以看到,僅僅是在promise化的異步api前有個yield
標識符,就可以使co
完美運作。上一篇我們也假想過co
的內部是如何實現的,我們再理(fu)順(zhi)一次:
- 我們調用遍歷器的
next()
得到該異步的promise對象 - 在promise對象的
then()
中的resolve
對數據進行處理 - 把處理后的數據作為參數
res
傳入next(res)
,繼續到下一次異步操作 - 重復2,3步驟。直到迭代器的
done: true
,結束遍歷。
如果不清楚我們上面說過的Generator遍歷器或promise對象的,可以先放一放這篇文章,從之前的幾篇看起。
進入co的世界
獲得遍歷器
co的源碼包括注釋和空行僅僅才240行,不能再精簡!我們抽出其中主要的代碼來進行分析。
function co(gen) {
var ctx = this; // context
// return a promise
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx); // 調用構造器來獲得遍歷器
if (!gen || typeof gen.next !== 'function') return resolve(gen);
//...下面代碼暫時省略...
})
}
這里我們需要關注的有兩點:
- co函數最終返回的是一個Promise。
- 第6行代碼,我們可以看到gen變量一開始就已經自身調用了。也就是gen從構造器變成了遍歷器。
遍歷器開始遍歷
我們首先看看co
內部的next(ret)
函數,它是整個遍歷器自動運行的關鍵。
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) + '"'));
}
我們可以看到,ret參數有done
和value
,那么ret肯定就是遍歷器每次next()
的結果。如果發現遍歷器遍歷結束的話,便直接return整個大Promise的resolve(ret.value)
方法結束遍歷。對了,此遍歷器的next()
和co的next()
在這里是不一樣的。當然你可以認為co將遍歷器的next()
又封裝了一遍方便源碼使用。
接着看,如果並沒有完成遍歷。我們就會對ret.value
調用toPromise()
,這里有知識點延伸,暫且先跳過,因為我們 一個 promise化的異步操作就是返回promise的。不知道大家get到point沒?我就透漏一點,當是數組或對象時,co
會識別並支持多異步的並行操作,先不管~~
我們在保證我們調用異步操作得到的value
是promise后,我們就會調用value.then()
方法為promise的onFulfilled()
或onRejected()
進行回調的綁定。也就是說,這段時間程序都是在干其他和遍歷器無關的事的。遍歷器沒有得到遍歷器的next()
指令,就一直靜靜的等着。我們可以想到,next()
指令,必定是放在了那兩個回調函數(onFulfilled
,onRejected
)里。
自動運行
promise化的異步API是先綁定了回調方法,然后等待異步完成后進行觸發。所以我們把遍歷器繼續遍歷的next()
指令放在回調中,就可以達到回調返回數據后再調用遍歷器next()
指令,遍歷器才會繼續下一個異步操作。
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 遍歷器進行遍歷,ret是此次遍歷項
} catch (e) {
return reject(e);
}
next(ret); // ret.value is a promise
}
我們看到第四行,通過調用遍歷器的next(res)
,再次啟動遍歷器得到新的遍歷結果,再傳入co
的next()
里,重復之前的操作,達到自動運行的效果。這里需要注意一個地方,我們是通過向遍歷器的next(res)
傳入res
變量來實現將異步執行后的數據保存到遍歷器里。
理解的關鍵
我相信我不可能說的很明白,讓大家一下子就知道關鍵重點是哪個。我自己也是悟了不少時間的,最終發現那個可以使思路清晰的就是Deferred
延遲對象。我在第二篇也有着重說過Deferred
延遲對象,它最重要的一點就是,它是用來延遲觸發回調的。我們先通過延遲對象的promise進行回調的綁定,然后在Node的異步操作的回調中觸發promise綁定的函數,實現異步操作。當然這里也是如此,我們是把遍歷器的next()
指令延遲到回調時再觸發。當然在co
源碼里是直接使用了ES6的promise原生對象,我們看不到deferred
的存在。
所以我很早前就說了,promise對理解co
至關重要。之前在promise上也花費了特別大的精力去理解,並分析原理。所以大家如果沒有看之前的有關promise文章的,最好都回去看一看,絕對有好處!
co其他的內容
分析完co
最關鍵的部分,接下來就是其他各種有用的源碼分析。關於thunk
轉化為promise
我就不說了,畢竟它也是被淘汰了的東西。那要說的東西其實就兩個,一個是多異步並行,一個是將co-generator
轉化為常規函數。我們一個一個來講:
多異步並行
之前也有提到過,就是我們需要對迭代對象的值進行toPromise()
操作。這個操作顧名思義,就是將所有需要yield的值,通通轉化為promise對象。它的源碼就是這樣的,並不能看到實質的東西:
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
我們還記得在co
的next()
函數里可以看到有一個注釋是這樣的:
'You may only yield a function, promise, generator, array, or object'
意思是,我們不僅僅只可以yield一個promise對象。function和promise我們就不說了,重點就是在array和object上,它們都是通過遞歸調用toPromise()
來實現每一個並行操作都是promise化的。
數組Array
我們先看看相對簡單的array的源碼:
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
map是ES5的array的方法,這個相信也有人經常使用的。我們將數組里的每一項的值,再進行一次toPromise
操作,然后得到全部都是promise對象的數組交給Promise.all
方法使用。這個方法在promise文章的第二篇也講過它的實現,它會在所有異步都執行完后才會執行回調。最后resolve(res)
的res
是一個存有所有異步操作執行完后的值的數組。
對象Object
Object就相對復雜些,不過原理依然是大同小異的,最后都是回歸到一個promise數組然后使用Promise.all()
。使用Object的好處就是,異步操作的名字和值是可以對應起來的,來看看代碼:
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj); // 得到的是一個存對象keys名字的數組
var promises = []; // 用於存放promise
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;
}));
}
}
第一個就是新建一個和傳入的對象一樣構造器的對象(這個寫法太厲害了)。我們先獲得了對象的所有的keys屬性名,然后根據keys,來獲取到每一個對象的屬性值。一樣是用toPromise()
讓屬性值——也就是並行操作promise化,當然非promise的值就會直接存到results這個對象里。如果是promise,就會執行內部定義的defer(promise, key)
函數。
所以理解defer函數是關鍵,我們看到是在defer函數里,我們才將當前的promise推入到promises數組里。並且每一個promise都是綁定了一個resolve()
方法的,就是將結果保存到results
的對象中。最后我們就得到一組都是promise的數組,通過Promise.all()
方法進行異步並行操作,這樣每個promise的結果都會保存到result對象相應的key里。而我們需要進行數據操作的也就是那個對象里的數據。
這里強烈建議大家動手模擬實現一遍 objectToPromise。
co.wrap(*generatorFunc)
下一個很有用的東西就是co.wrap()
,它允許我們將co-generator
函數轉化成常規函數,我覺得這個還是需要舉例子來表明它的作用。假設我們有多個異步的讀取文件的操作,我們用co來實現。
//讀取文件1
co(function* (){
var filename = yield readFile('hello1.txt', 'utf-8');
return filename;
}).then(console.log, console.error);
//讀取文件2
co(function* (){
var filename = yield readFile('hello2.txt', 'utf-8');
return filename;
}).then(console.log, console.error);
天啊,我仿佛又回到了不會使用函數的年代,一個功能一段函數,不能復用。當然co.wrap()
就是幫你解決這個問題的。
var getFile = co.wrap(function* (file){
var filename = yield readFile(file, 'utf-8');
return filename;
});
getFile('hello.txt').then(console.log);
getFile('hello2.txt').then(console.log);
例子很簡單,我們可以將co-generator
里的變量抽取出來,形成一個常規的Promise函數(regular-function)。這樣子就無論是復用性還是代碼結構都是優化了不少。
既然知道了怎么用,就該看看它內部如何實現的啦,畢竟這是一次源碼分析。其實如果對函數柯里化(偏函數)比較了解,就會覺得非常簡單。
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn; // 這個應該是像函數constructor的東西
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
就是一個偏函數,借助於高階函數的特性,返回一個新函數createPromise()
,然后傳給它的參數都會被導入到Generator函數中。