瞭解
co
的前提是已經知曉generator
是什麼,能夠看軟大神的Generator 函數的語法,
co是TJ大神寫的可以使generator自動執行的函數庫,而咱們熟知的koa也用到了它管理異步流程控制,將異步任務書寫同步化,爽的飛起,也擺脫了一直以來的回調地獄問題。java
首先咱們根據co的官方文檔來稍作改變看下,到底如何使用co,再一步步進行源碼分析工做(這篇文章分析的co版本是
4.6.0
)。git
yield 後面常見的能夠跟的類型es6
promisesgithub
let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)
co(function * () {
let a = yield timeout1()
console.log(a) // delayTime: 1000
let b = yield timeout2()
console.log(b) // delayTime: 200
return 'end'
}).then((res) => {
console.log(res)
})複製代碼
arrayapi
let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)
co(function * () {
let a = yield [timeout1(), timeout2()]
console.log(a) // [ 'delayTime: 1000', 'delayTime: 200' ]
return 'end'
}).then((res) => {
console.log(res) // end
})複製代碼
objects數組
let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)
co(function * () {
let a = yield {
timeout1: timeout1(),
timeout2: timeout2()
}
console.log(a) // { timeout1: 'delayTime: 1000',timeout2: 'delayTime: 200' }
return 'end'
}).then((res) => {
console.log(res) // end
})複製代碼
generator functionspromise
let co = require('co')
let genTimeoutFun = (delay) => {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`delayTime: ${delay}`)
}, delay)
})
}
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)
function * gen () {
let a = yield timeout1()
console.log(a) // delayTime: 1000
let b = yield timeout2()
console.log(b) // delayTime: 200
}
co(function * () {
yield gen()
return 'end'
}).then((res) => {
console.log(res) // end
})複製代碼
最後說一下,關於執行傳入的generator函數接收參數的問題app
let co = require('co')
co(function * (name) {
console.log(name) // qianlongo
}, 'qianlongo')複製代碼
從co函數的第二個參數開始,即是傳入的generator函數能夠接收的實參koa
你能夠把以上代碼拷貝至本地測試一番看看效果,接下來咱們一步步開始分析co的源碼
首先通過上面的例子能夠發現,co函數自己接收一個generator函數,而且co執行後返回的是Promise
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
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// xxx
});
}複製代碼
在Promise的內部,先執行了外部傳入的gen
,執行的結果若是不具有next屬性(且要是一個函數),就直接返回,並將執行成功回調resolve(gen)
,不然獲得的是一個指針對象。
接下來繼續看...
onFulfilled();
/** * @param {Mixed} res * @return {Promise} * @api private */
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 用上面執行gen以後的generator生成器將指針指向下一個位置
} catch (e) {
return reject(e);
}
next(ret); // 緊接着執行next,正是它實現了反覆調用本身,自動流程控制,注意ret(即上一次gen.next執行後返回的對象{value: xxx, done: true or false})
}
/** * @param {Error} err * @return {Promise} * @api private */
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}複製代碼
我以爲能夠把 onFulfilled
和 onRejected
當作是返回的Promise的resolve
和reject
。
而onFulfilled
也是將原生的generator生成器的next方法包裝了一遍,大概是爲了抓取錯誤吧(看到內部的try catch了嗎)
好,咱們看到了co內部將指針移動到了第一個位置以後,接着執行了內部的next方法,接下來聚焦在該函數上
function next(ret) {
// 若是整個generator函數的內部狀態已經表示走完,便將Promise的狀態設置爲成功狀態,並執行resolve
if (ret.done) return resolve(ret.value);
// 這一步是將ret的value轉換爲Promise形式
var value = toPromise.call(ctx, ret.value);
// 這裏很是關鍵,是co實現本身調用本身,實現流程自動化的關鍵
// 注意這裏使用value.then,即爲返回值添加成功和失敗的回調,在成功的回調裏面再去執行onFulfilled,緊接着就是調用內部的next函數
// 那不是就保證了流程徹底按照你寫的順序來了?
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// 拋出錯誤,yield後只能跟着指定的下列這幾種類型
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是怎麼將異步流程自動管理起來了
可是我對next函數中的toPromise函數還有疑問,他到底作了什麼事?使得co(generatorFun)中yield能夠支持數組、對象、generator函數等形式。
一步步來看
function toPromise(obj) {
// obj不存在,直接返回
if (!obj) return obj;
// 若是obj已是Promise,則也是直接返回
if (isPromise(obj)) return obj;
// 若是是個generator函數或者generator生成器,那就像你本身調用co函數同樣,手動傳到co裏面去執行
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// 若是obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一個普通的函數(須要符合thunk函數規範),就將該函數包裝成Promise的形式
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
// 若是是一個數組的形式,就去arrayToPromise包裝一番
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}複製代碼
首先若是obj不存在,就直接返回,你想啊,co原本就是依賴上一次指針返回的value是Promise或者其餘,這個時候若是返回
{
value: false,
done: false
}複製代碼
那就沒有必要再給一個false值轉成Promise形式了吧。
接着,若是obj自己就是個Promise也是直接返回,用了內部的isPromise函數進行判斷,咱們看下他怎麼實現的。
function isPromise(obj) {
return 'function' == typeof obj.then;
}複製代碼
其實就是判斷了obj的then屬性是否是個函數
再接着,若是是個generator函數或者generator生成器,那就像你本身調用co函數同樣,手動傳到co裏面去執行。
isGeneratorFunction
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}複製代碼
經過obj的constructor屬性去判斷其是否屬於GeneratorFunction
,最後若是constructor屬性沒判斷出來,再去用isGenerator,判斷obj的原型是否是generator生成器
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}複製代碼
判斷的條件也比較直接,須要符合兩個條件,一個是obj.next要是一個函數,一個是obj.throw要是一個函數
接下來繼續看
若是obj既不是Promise,也不是isGeneratorFunction和isGenerator,要是一個普通的函數,就將該函數包裝成Promise的形式,這裏咱們主要須要看thunkToPromise
function thunkToPromise(fn) {
var ctx = this;
// 將thunk函數包裝成Promise
return new Promise(function (resolve, reject) {
// 執行這個thunk函數
fn.call(ctx, function (err, res) {
// 注意thunk函數內部接收的回調函數中傳入的第一個參數是err,出現了err,固然須要走reject了
if (err) return reject(err);
// 參數是兩個以上的狀況下,將參數整成一個數組
if (arguments.length > 2) res = slice.call(arguments, 1);
// 最後執行成功的回調
resolve(res);
});
});
}複製代碼
接下來是重頭戲了,co中若是處理yield後面跟一個數組呢?主要是arrayToPromise函數的做用
function arrayToPromise(obj) {
// 使用到了Promise.all,將obj中多個promise實例(固然你也能夠在數組中填thunk函數,generator函數等)從新包裝成一個。最後返回一個新的Promise
return Promise.all(obj.map(toPromise, this));
}複製代碼
還有最後一個判斷,若是obj是個對象怎麼辦?
function objectToPromise(obj){
// 構造一個和傳入對象有相同構造器的對象, results也是
var results = new obj.constructor();
// 獲取obj的keys
var keys = Object.keys(obj);
// 存儲obj中是Promise的屬性
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
// 若是是結果是Promise,則用defer函數對results進行修改
if (promise && isPromise(promise)) defer(promise, key);
// 若是是非Promise就按原樣返回
else results[key] = obj[key];
}
// 最後 使用到了Promise.all,將obj中多個promise實例
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
results[key] = res;
}));
}
}複製代碼
到這裏,co源碼分析就告一段落了。總感受有些沒有說到位,歡迎你們拍磚,晚安。