本文始發於個人我的博客,如需轉載請註明出處。
爲了更好的閱讀體驗,能夠直接進去個人我的博客看。html
閱讀本文須要對Generator
和Promise
有一個基本的瞭解。node
這裏我簡單地介紹一下二者的用法。git
關於Generator的用法,推薦MDN上面的解釋function *函數,裏面很是詳細。github
用一句話總結就是,generator函數
是回調地獄的一種解決方案,它跟promise
相似,可是卻能夠以同步的方式來書寫代碼,而避免了promise的鏈式調用。api
它的執行過程在於調用生成器函數(generator function)
後,會返回一個iterator(迭代)對象
,即Generator對象
,可是它並不會馬上執行裏面的代碼。數組
它有幾個方法,next()
, throw()
和return()
。調用next()方法後,它會找到第一個yield關鍵字(直到找到程序底部或者return語句),每次程序運行到yield關鍵字時,程序便會暫停,保存當前環境裏面的變量的值,而後能夠跳出當前運行環境去執行yield後面的代碼,再把結果返回回來。promise
返回的結果是一個對象,相似於{value: '', done: false}
, value表示本次yield後面執行以後返回的結果。若是是Promise實例,則是返回resolved後的值。done表示迭代器是否執行完畢,若爲true
,則表示當前生成器函數已經產生了最後輸出的值,即生成器函數已經返回。app
下面是一個簡單的例子:框架
const gen = function *() { let index = 0; while(index < 3) yield index++; return 'All done.' }; const g = gen(); console.log(g.constructor); // output: GeneratorFunction {} console.log(g.next()); // output: { value: 0, done: false } console.log(g.next()); // output: { value: 1, done: false } console.log(g.next()); // output: { value: 2, done: false } console.log(g.next()); // output: { value: 'All done.', done: true } console.log(g.next()); // output: { value: undefined, done: true }
關於Promise
的用法,能夠查閱我以前寫過的一篇文章《關於ES6中Promise的用法》,寫得比較詳細。koa
Promise對象用於一個異步操做的最終完成(或失敗)及其結果值的表示(簡單點說就是處理異步請求)。Promise核心就在於裏面狀態的變換,是rejected
、resolved
仍是pending
,還有就是原型鏈上的then()
方法,它能夠傳遞本次狀態轉換後返回的值。
因爲實際須要,這幾天學習了koa2.x
框架,可是它已經不推薦使用generator函數了,推薦用async/await
組合。
koa2.x的最新用法:
async/await(node v7.6+):
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });
common 用法:
const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
因爲本地的Node版本是v6.11.5
,而使用async/await則須要Node版本v7.6
以上,因此我想有沒有什麼模塊可以把koa2.x版本的語法兼容koa1.x的語法。koa1.x語法的關鍵在於generator/yield
組合。經過yield能夠很方便地暫停程序的執行,並改變執行環境。
這時候我找到了TJ大神寫的co模塊
,它可讓異步流程同步化,還有koa-convert
模塊等等,這裏着重介紹co模塊。
co在koa2.x裏面的用法以下:
const Koa = require('koa'); const app = new Koa(); const co = require('co'); // response app.use(co.wrap(function *(ctx, next) { yield next(); // yield someAyncOperation; // ... ctx.body = 'co'; })); app.listen(3000);
co模塊不只能夠配合koa框架充當中間件的轉換函數使用,還支持批量執行generator函數,這樣就無需手動調用屢次next()來獲取結果了。
它支持的參數有函數、promise、generator、數組和對象
。
// co的源碼 return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"'));
下面舉一個co傳遞進來一個generator函數的例子:
// 這裏模擬一個generator函數調用 const co = require('co'); co(gen).then(data => { // output: then: ALL Done. console.log('then: ' + data); }); function *gen() { let data1 = yield pro1(); // output: pro1 had resolved, data1 = I am promise1 console.log('pro1 had resolved, data1 = ' + data1); let data2 = yield pro2(); // output: pro2 had resolved, data2 = I am promise2 console.log('pro2 had resolved, data2 = ' + data2); return 'ALL Done.' } function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'I am promise1'); }); } function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'I am promise2'); }); }
我以爲co()函數很神奇,裏面究竟通過了什麼樣的轉換?抱着一顆好奇心,讀了一下co的源碼。
co函數調用後,返回一個Promise實例。
co的思想就是將一個傳遞進來的參數進行合法化,再經過轉換成Promise實例返回出去。若是參數fn是generator函數
的話,裏面還能夠自動進行遍歷,執行generator函數裏面的yield關鍵字後面的內容,並返回結果,也就是不斷地調用fn().next()
方法,再經過傳遞返回的Promise實例resolved
後的值,從而達到同步執行generator函數的效果。
這裏要注意,co裏面最主要的是要理解Promise實例和Generator對象,它們是co函數裏面的程序自動遍歷執行的關鍵。
下面解釋一下co模塊裏面的最重要的兩部分,一個是generator函數的自動調用,另一個是參數的Promise化。
第一,generator函數的自動調用(中文部分是個人解釋):
function co(gen) { // 保存當前的執行環境 var ctx = this; // 切割出函數調用時傳遞的參數 var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 // 返回一個Promise實例 return new Promise(function(resolve, reject) { // 若是gen是一個函數,則返回一個新的gen函數的副本, // 裏面綁定了this的指向,即ctx if (typeof gen === 'function') gen = gen.apply(ctx, args); // 若是gen不存在或者gen.next不是一個函數 // 就說明gen已經調用完成, // 那麼直接能夠resolve(gen),返回Promise if (!gen || typeof gen.next !== 'function') return resolve(gen); // 首次調用gen.next()函數,假如存在的話 onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { // 嘗試着獲取下一個yield後面代碼執行後返回的值 ret = gen.next(res); } catch (e) { return reject(e); } // 處理結果 next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { // 嘗試拋出錯誤 ret = gen.throw(err); } catch (e) { return reject(e); } // 處理結果 next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ // 這個next()函數是最爲關鍵的一部分, // 裏面幾乎包含了generator自動調用實現的核心 function next(ret) { // 若是ret.done === true, // 證實generator函數已經執行完畢 // 即已經返回了值 if (ret.done) return resolve(ret.value); // 把ret.value轉換成Promise對象繼續調用 var value = toPromise.call(ctx, ret.value); // 若是存在,則把控制權交給onFulfilled和onRejected, // 實現遞歸調用 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) + '"')); } }); }
對於以上代碼中的onFulfilled
和onRejected
,咱們能夠把它們當作是co模塊對於resolve
和reject
封裝的增強版。
第二,參數Promise化,咱們來看一下co中的toPromise的實現:
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; }
toPromise的本質上就是經過斷定參數的類型,而後再經過轉移控制權給不一樣的參數處理函數,從而獲取到指望返回的值。
關於參數的類型的判斷,看一下源碼就能理解了,比較簡單。
咱們着重來分析一下objectToPromise的實現:
function objectToPromise(obj){ // 獲取一個和傳入的對象同樣構造器的對象 var results = new obj.constructor(); // 獲取對象的全部能夠遍歷的key var keys = Object.keys(obj); var promises = []; for (var i = 0; i < keys.length; i++) { var key = keys[i]; // 對於數組的每個項都調用一次toPromise方法,變成Promise對象 var promise = toPromise.call(this, obj[key]); // 若是裏面是Promise對象的話,則取出e裏面resolved後的值 if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; } // 並行,按順序返回結果,返回一個數組 return Promise.all(promises).then(function () { return results; }); // 根據key來獲取Promise實例resolved後的結果, // 從而push進結果數組results中 function defer(promise, key) { // predefine the key in the result results[key] = undefined; promises.push(promise.then(function (res) { results[key] = res; })); } }
上面理解的關鍵就在於把key遍歷,若是key
對應的value
也是Promise
對象的話,那麼調用defer()
方法來獲取resolved
後的值。
經過以上的簡單介紹,咱們就能夠嘗試來寫一個屬於本身的generator函數運行器了,目標功能是可以自動運行function*
函數,而且裏面的yield子句
後面跟着的都是Promise實例
。
具體代碼(my-co.js
)以下:
// my-co.js module.exports = my-co; let my-co = function (gen) { // gen是一個具備Promise的生成器函數 const g = gen(); // 迭代器 // 首次調用next next(); function next(val) { let ret = g.next(val); // 調用ret if (ret.done) { return ret.value; } if (ret && 'function' === typeof ret.value.then) { ret.value.then( (data) => { // 繼續循環下去 return next(data); // promise resolved }); } } };
這樣咱們就能夠在test.js
文件中調用了:
// test.js const myCo = require('./my-co'); const fs = require('fs'); let gen = function *() { let data1 = yield pro1(); console.log('data1: ' + data1); let data2 = yield pro2(); console.log('data2: ' + data2); let data3 = yield pro3(); console.log('data3: ' + data3); let data4 = yield pro4(data1 + '\n' + data2 + '\n' + data3); console.log('data4: ' + data4); return 'All done.' }; // 調用myCo myCo(gen); // 延遲兩秒resolve function pro1() { return new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'promise1 resolved'); }); } // 延遲一秒resolve function pro2() { return new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'promise2 resolved'); }); } // 寫入Hello World到./1.txt文件中 function pro3() { return new Promise((resolve, reject) => { fs.appendFile('./1.txt', 'Hello World\n', function(err) { resolve('write-1 success'); }); }); } // 寫入content到./1.txt文件中 function pro4(content) { return new Promise((resolve, reject) => { fs.appendFile('./1.txt', content, function(err) { resolve('write-2 success'); }); }); }
控制檯輸出結果:
// output data1: promise1 resolved data2: promise2 resolved data3: write-1 success data4: write-2 success
./1.txt
文件內容:
Hello World promise1 resolved promise2 resolved write-1 success
由上可知,運行的結果符合咱們的指望。
雖然這個運行器很簡單,後面只支持Promise實例,而且也不支持多種參數,可是卻引導出了一個思路,促使咱們思考怎麼去展現咱們的代碼,還有就是頗有效地避免了多重then,以同步的方式來書寫異步代碼。Promise解決的是回調地獄
的問題(callback hell
),而Generator解決的是代碼的書寫方式。孰優孰劣,全在於我的意願。
以上分析了co部分源碼的精髓,講到了co函數裏面generator函數自動遍歷執行的機制,還講到了co裏面最爲關鍵的objectToPromise()
方法。
在文章的後面咱們編寫了一個屬於本身的generator函數遍歷器,其中主要的是next()方法,它能夠檢測咱們yield後面Promise操做是否完成。若是generator的狀態done
尚未置爲true
,那麼繼續調用next(val)
方法,並把上一次yield
操做獲取到的值傳遞下去。
有時候在引用別人的模塊出現問題時,若是在網上找不到本身指望的答案,那麼咱們能夠根據本身的能力來選擇性地分析一下做者的源碼,看源碼是一種很好的成長方式。
坦白說,這是我第一次深刻分析模塊的源碼,co模塊的源碼包括註釋和空行只有230多行
左右,因此這是一個很好的切入點。裏面代碼雖少,可是理解卻不易。
若是以上所述有什麼問題,歡迎反饋。
感謝支持。