co 函式庫的含義和用法

阮一峰發表於2015-05-06

進入正文之前,先插播一條訊息。

我七年前翻譯的《軟體隨想錄》再版了(京東連結)。這次是《Joel論軟體》兩卷同時再版,第一卷是新譯本,第二卷是我翻譯的。

本書的作者是著名程式設計師、StackOverflow的創始人 Joel Splosky。我覺得,它是軟體專案管理的最好讀物之一,推薦閱讀。

========================================

以下是《深入掌握 ECMAScript 6 非同步程式設計》系列文章的第三篇。

一、什麼是 co 函式庫?

co 函式庫是著名程式設計師 TJ Holowaychuk 於2013年6月釋出的一個小工具,用於 Generator 函式的自動執行。

比如,有一個 Generator 函式,用於依次讀取兩個檔案。


var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

co 函式庫可以讓你不用編寫 Generator 函式的執行器。


var co = require('co');
co(gen);

上面程式碼中,Generator 函式只要傳入 co 函式,就會自動執行。

co 函式返回一個 Promise 物件,因此可以用 then 方法新增回撥函式。


co(gen).then(function (){
  console.log('Generator 函式執行完成');
})

上面程式碼中,等到 Generator 函式執行結束,就會輸出一行提示。

二、 co 函式庫的原理

為什麼 co 可以自動執行 Generator 函式?

前面文章說過,Generator 函式就是一個非同步操作的容器。它的自動執行需要一種機制,當非同步操作有了結果,能夠自動交回執行權。

兩種方法可以做到這一點。

(1)回撥函式。將非同步操作包裝成 Thunk 函式,在回撥函式里面交回執行權。

(2)Promise 物件。將非同步操作包裝成 Promise 物件,用 then 方法交回執行權。

co 函式庫其實就是將兩種自動執行器(Thunk 函式和 Promise 物件),包裝成一個庫。使用 co 的前提條件是,Generator 函式的 yield 命令後面,只能是 Thunk 函式或 Promise 物件。

上一篇文章已經介紹了基於 Thunk 函式的自動執行器。下面來看,基於 Promise 物件的自動執行器。這是理解 co 函式庫必須的。

三、基於 Promise 物件的自動執行

還是沿用上面的例子。首先,把 fs 模組的 readFile 方法包裝成一個 Promise 物件。


var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

然後,手動執行上面的 Generator 函式。


var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
})

手動執行其實就是用 then 方法,層層新增回撥函式。理解了這一點,就可以寫出一個自動執行器。


function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

上面程式碼中,只要 Generator 函式還沒執行到最後一步,next 函式就呼叫自身,以此實現自動執行。

四、co 函式庫的原始碼

co 就是上面那個自動執行器的擴充套件,它的原始碼只有幾十行,非常簡單。

首先,co 函式接受 Generator 函式作為引數,返回一個 Promise 物件。


function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的 Promise 物件裡面,co 先檢查引數 gen 是否為 Generator 函式。如果是,就執行該函式,得到一個內部指標物件;如果不是就返回,並將 Promise 物件的狀態改為 resolved 。


function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
  });
}

接著,co 將 Generator 函式的內部指標物件的 next 方法,包裝成 onFulefilled 函式。這主要是為了能夠捕捉丟擲的錯誤。


function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }    
  });
}

最後,就是關鍵的 next 函式,它會反覆呼叫自身。


function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = toPromise.call(ctx, ret.value);
  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) + '"'));
    }
});

上面程式碼中,next 函式的內部程式碼,一共只有四行命令。

第一行,檢查當前是否為 Generator 函式的最後一步,如果是就返回。

第二行,確保每一步的返回值,是 Promise 物件。

第三行,使用 then 方法,為返回值加上回撥函式,然後透過 onFulfilled 函式再次呼叫 next 函式。

第四行,在引數不符合要求的情況下(引數非 Thunk 函式和 Promise 物件),將 Promise 物件的狀態改為 rejected,從而終止執行。

五、併發的非同步操作

co 支援併發的非同步操作,即允許某些操作同時進行,等到它們全部完成,才進行下一步。

這時,要把併發的操作都放在陣列或物件裡面。


// 陣列的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res); 
}).catch(onerror);

// 物件的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res); 
}).catch(onerror);

(完)

相關文章