co源碼分析及其實踐

本文始發於個人我的博客,如需轉載請註明出處。
爲了更好的閱讀體驗,能夠直接進去個人我的博客看。html

前言

知識儲備

閱讀本文須要對GeneratorPromise有一個基本的瞭解。node

這裏我簡單地介紹一下二者的用法。git

Generator

關於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

關於Promise的用法,能夠查閱我以前寫過的一篇文章《關於ES6中Promise的用法》,寫得比較詳細。koa

Promise對象用於一個異步操做的最終完成(或失敗)及其結果值的表示(簡單點說就是處理異步請求)。Promise核心就在於裏面狀態的變換,是rejectedresolved仍是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源碼分析

主要脈絡

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) + '"'));
    }
  });
}

對於以上代碼中的onFulfilledonRejected,咱們能夠把它們當作是co模塊對於resolvereject封裝的增強版。

第二,參數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函數運行器

經過以上的簡單介紹,咱們就能夠嘗試來寫一個屬於本身的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多行左右,因此這是一個很好的切入點。裏面代碼雖少,可是理解卻不易。

若是以上所述有什麼問題,歡迎反饋。

感謝支持。

參考連接

  1. MDN - Promise解釋
  2. MDN - Generator對象的用法
  3. TJ - co的源碼及其用法
相關文章
相關標籤/搜索