異步編程系列教程:git
你們若是能消化掉前面的知識,相信這一章的分析也確定是輕輕鬆鬆的。咱們這一章就來講說,咱們以前一直高調提到的co
庫。co
庫,它用Generator和Promise相結合,完美提高了咱們異步編程的體驗。咱們首先看看如何使用co
的,咱們仍舊以以前的讀取Json文件的例子看看:github
// 注意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)一次:json
next()
獲得該異步的promise對象then()
中的resolve
對數據進行處理res
傳入next(res)
,繼續到下一次異步操做done: true
,結束遍歷。若是不清楚咱們上面說過的Generator遍歷器或promise對象的,能夠先放一放這篇文章,從以前的幾篇看起。api
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); //...下面代碼暫時省略... }) }
這裏咱們須要關注的有兩點:promise
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()
又封裝了一遍方便源碼使用。app
接着看,若是並無完成遍歷。咱們就會對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
最關鍵的部分,接下來就是其餘各類有用的源碼分析。關於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的源碼:
function arrayToPromise(obj) { return Promise.all(obj.map(toPromise, this)); }
map是ES5的array的方法,這個相信也有人常用的。咱們將數組裏的每一項的值,再進行一次toPromise
操做,而後獲得所有都是promise對象的數組交給Promise.all
方法使用。這個方法在promise文章的第二篇也講過它的實現,它會在全部異步都執行完後纔會執行回調。最後resolve(res)
的res
是一個存有全部異步操做執行完後的值的數組。
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函數中。