走一步再走一步,揭開co的神祕面紗

前言

原文地址
源碼地址javascript

瞭解co的前提是已經知曉generator是什麼,能夠看軟大神的Generator 函數的語法,
co是TJ大神寫的可以使generator自動執行的函數庫,而咱們熟知的koa也用到了它管理異步流程控制,將異步任務書寫同步化,爽的飛起,也擺脫了一直以來的回調地獄問題。java

如何使用

首先咱們根據co的官方文檔來稍作改變看下,到底如何使用co,再一步步進行源碼分析工做(這篇文章分析的co版本是4.6.0)。git

yield 後面常見的能夠跟的類型es6

  1. promises
  2. array (parallel execution)
  3. objects (parallel execution)
  4. generator functions (delegation)

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);
}複製代碼

我以爲能夠把 onFulfilledonRejected 當作是返回的Promise的resolvereject

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源碼分析就告一段落了。總感受有些沒有說到位,歡迎你們拍磚,晚安。

相關文章
相關標籤/搜索