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

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
