1. 程式人生 > >co模組用法及分析

co模組用法及分析

轉載:https://segmentfault.com/a/1190000007792512

寫在前面

學 nodejs 當然避免不了學習框架,畢竟原生的 API 較底層。最先接觸的是 Koa 。看看官網的描述

next generation web framework for node.js

我翻譯一下就是: 基於 node.js 的下一代 web 開發框架。好像很厲害的樣子!koa 是一個輕量級的框架,本質上提供了一個架子,通過 各種中介軟體的級聯的方式實現特定的功能。koa 藉助 promise 和 generator , 很好解決了非同步組合問題。

那什麼又是 co 。學習 koa 就一定少不了學習 co 模組。co 模組可以將非同步解放成同步。co 函式接受一個 generator 函式作為引數,在函式內部自動執行 yield 。

co 原始碼分析

使用的 co 模組版本號為 4.6.0

首先看一些用於判斷物件型別的函式

var slice = Array.prototype.slice; // 對陣列 slice 方法的引用 
function isObject(val) {
  return Object == val.constructor;
}

這兩個應該就不用說了吧。。。

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

判斷一個物件是否是一個 promise 例項,判斷的依據也很簡單,根據 “鴨子型別”,判斷這個物件是否有 then 方法

function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}

類似的,判斷一個物件時候是 generator 例項,只需判斷這個物件是否具有 next 方法和 throw 方法。

function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor
.name || 'GeneratorFunction' === constructor.displayName) return true; return isGenerator(constructor.prototype); }

判斷是否是一個 generator 函式,只需判斷這個函式是否是 GeneratorFunction 函式的例項

以上所講的在之後將 value 包裝成 promise 例項時都會用到。

看一下 co 模組的輸出部分

module.exports = co['default'] = co.co = co

因此以下三種用法等價

var co = require('co') // (1)
var co = require('co').co // (2)
var co = require('co').default // (3)

接著就是重頭戲 co 函數了。

function co(gen) {
  var ctx = this; // 儲存函式的執行上下文物件
  var args = slice.call(arguments, 1) // 傳給 gen 函式的引數
  // 返回一個 promise 例項
  return new Promise(function(resolve, reject) {
       // 根據傳入的 generator 函式生成一個 generator 例項
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 如果生成的 gen 不是一個 generator 例項, 
    // promise 直接變成 resolved 狀態
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
     // 執行 onFulfilled 方法
    onFulfilled();

    function onFulfilled(res) {
      var ret;
      try {
        // 執行 gen 的 next 方法
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      // 並將這個值傳入 next 函式
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      // 如果 gen 執行完畢, ret.done 變為 true ,那麼這個 promise 的例項
      // 的狀態自然變成了 resolved
      if (ret.done) return resolve(ret.value); 
      var value = toPromise.call(ctx, ret.value); // 將 value 重新包裝成一個 promise 例項
      // 新返回的 promise 例項的 resolve 方法設定為 onFulfilled 函式,再次執行 next 方法, 從而實現了自動呼叫 generator 例項的 next 方法
      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) + '"'));
    }
  });
}

以上,就是 co 模組就實現了自動執行 generator 例項的 next 方法。那麼接下來看看 co 是怎麼把一個值轉化為一個 promise 例項。

function toPromise(obj) {
  if (!obj) return obj;  // 如果傳入的 obj 是假值,返回這個假值 如 undefined , false, null
  if (isPromise(obj)) return obj; // 如果是 Promise 例項,返回這個 promise 例項
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); // 如果是 generator 函式或者 一個generator
  if ('function' == typeof obj) return thunkToPromise.call(this, obj); // 如果是 thunk 函式
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj); // 如果是一個數組
  if (isObject(obj)) return objectToPromise.call(this, obj); // 如果是一個 plain object
  return obj; // 如果是原始值,則返回這個原始值。
}

那麼每個函式依次看下去。

function thunkToPromise(fn) {
  var ctx = this; // 儲存函式上下文物件
  // 返回一個 promise 例項
  return new Promise(function (resolve, reject) {
    // 執行傳入的 thunk 函式
    // thunk 函式接受一個 回撥函式 作為引數
    fn.call(ctx, function (err, res) {
        // 如果 thunk 函式執行錯誤
        // promise 例項的 變為 rejected 狀態,執行 reject 函式,也就是 co 函式內定義的 onRejected 函式,下同
      if (err) return reject(err);
          // 獲得多餘引數
      if (arguments.length > 2) res = slice.call(arguments, 1);
      // promise 狀態變為 resolved ,執行 resolve 函式,也就是 onFulfilled 函式
      resolve(res);
    });
  });
}

所以,總結一下就是說,如果 generator 裡 yield 後面是一個 thunk 函式, 這個 thunk 函式接受一個回撥引數作為引數,co 在這個回撥函式裡定義了何時將 promise 的狀態變為 resolved 或者 rejected ,

function arrayToPromise(obj) {
  // Promise.all 方法返回一個 新的 promise 例項
  // 如果 obj 是一個數組,把每個元素包裝成一個 promise 例項
  // 如果每一個 promise 如果都變為 resolved 狀態
  // 那麼返回的新的 promise 例項的狀態變為 resloved 狀態
  // 傳給 resolve 函式的引數為之前每個 promise 的返回值所組成的陣列 
  return Promise.all(obj.map(toPromise, this)); 
}

同樣,如果 obj 是一個數組,也就是 yield 語句後面的表示式的值為一個數組,那麼就執行 Promise.all 方法, 將陣列的每一項都變成一個 promise 例項。

具體方法如下:

  1. 使用 toPromise 方法將 obj 陣列中的每一項都包裝成一個 promise 例項

  2. 如果上一步中的陣列中有元素不是 promise 例項,Promise.all 方法將呼叫 Promise.resolve 方法,將其轉化為 promise 例項。

  3. Promise.all 方法返回一個新的 promise 例項。

  4. 只有 promise 例項陣列中的所有例項的狀態都變為 resolved 狀態時,這個新的 promise 例項的狀態才會變成 resolved。只要陣列中有一個 promise 例項的狀態變為 rejected ,新的promise 例項狀態也馬上變為 rejected 。

  5. 當返回的新的 promise 例項狀態變為 resolved 時,傳入其 resolve 函式的引數為之前陣列中每個 promise 例項呼叫 resolve 函式的返回值組成的陣列。如果返回的新的 promise 的狀態變為 rejected ,那麼傳給 reject 函式的引數為陣列中的 promise 例項最先變為 rejected 狀態的那一個執行 reject 函式的返回值。

真繞口,多看幾遍應該就能理解了。

最後來看看如果 ret.value 如果是一個物件,co 模組是怎麼樣把它變成一個 promise 例項的。

function objectToPromise(obj){
  // 定義一個空物件
  var results = new obj.constructor();
  // 獲取 obj 的全部屬性
  var keys = Object.keys(obj);
  // 用於盛放 每個屬性值生成的對應的 promise 例項
  var promises = []; 
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]); // 根據屬性值生成一個 promise 例項
    if (promise && isPromise(promise)) defer(promise, key); 
    else results[key] = obj[key];
  }
  // 通過一個 promise.all 方法返回一個新的例項
  return Promise.all(promises).then(function () {
    return results; // 將 results 作為 onFulfilled 函式的引數
  });
  // 函式的作用
  // 給 promise 新增 resolve 函式
  // 並且把這個 promise 例項推入 promises 陣列
  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;

    promises.push(promise.then(function (res) {
      results[key] = res; // 定義promise 例項的 resolve 函式
    }));
  }
}

總結

分析完 co 的整個原始碼總結一下整個執行的過程。首先,co 函式接受一個 generator 函式,並且在 co 函式內部執行,生成一個 generator 例項。呼叫 generator 的 next 方法, 對生成的物件的 value 屬性值使用 toPromise 方法,生成一個 promise 例項,當這個 promise 例項的狀態變為 resolved 時,執行 onFulfilled 方法,再次對 generator 例項執行 next 方法,然後重複整個過程。如果出現錯誤,則執行這個 promise 例項定義的 reject 函式即 onRejected 方法。

以上即實現了將非同步過程同步化。

最後歡迎 star