異步編程之co——源碼分析


異步編程系列教程:

  1. (翻譯)異步編程之Promise(1)——初見魅力
  2. 異步編程之Promise(2):探究原理
  3. 異步編程之Promise(3):拓展進階
  4. 異步編程之Generator(1)——領略魅力
  5. 異步編程之Generator(2)——剖析特性
  6. 異步編程之co——源碼分析

如何使用co


大家如果能消化掉前面的知識,相信這一章的分析也肯定是輕輕松松的。我們這一章就來說說,我們之前一直高調提到的co庫。co庫,它用Generator和Promise相結合,完美提升了我們異步編程的體驗。我們首先看看如何使用co的,我們仍舊以之前的讀取Json文件的例子看看:

// 注意readFile已經是Promise化的異步API 
co(function* (){
    var filename = yield readFile('hello3.txt', 'utf-8');
    var json = yield readFile(filename, 'utf-8');
    return JSON.parse(json).message;
}).then(console.log, console.error);

大家看上面的代碼,甚至是可以使用同步的思維,不用去理會回調什么鬼的。我們readFile()得到filename,然后再次readFile()得到json,解析完json后輸出就結束了,非常清爽。大家如果不相信的話,可以使用原生的異步api嘗試一下,fs.readFile()像上面相互有依賴的,絕對惡心!

我們可以看到,僅僅是在promise化的異步api前有個yield標識符,就可以使co完美運作。上一篇我們也假想過co的內部是如何實現的,我們再理(fu)順(zhi)一次:

  1. 我們調用遍歷器的next()得到該異步的promise對象
  2. 在promise對象的then()中的resolve對數據進行處理
  3. 把處理后的數據作為參數res傳入next(res),繼續到下一次異步操作
  4. 重復2,3步驟。直到迭代器的done: true,結束遍歷。

如果不清楚我們上面說過的Generator遍歷器或promise對象的,可以先放一放這篇文章,從之前的幾篇看起。

進入co的世界


獲得遍歷器

co的源碼包括注釋和空行僅僅才240行,不能再精簡!我們抽出其中主要的代碼來進行分析。

function co(gen) {
  var ctx = this; // context
  
  // return a promise
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx); // 調用構造器來獲得遍歷器
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    
    //...下面代碼暫時省略...
   })
}

這里我們需要關注的有兩點:

  1. co函數最終返回的是一個Promise。
  2. 第6行代碼,我們可以看到gen變量一開始就已經自身調用了。也就是gen從構造器變成了遍歷器。


遍歷器開始遍歷

我們首先看看co內部的next(ret)函數,它是整個遍歷器自動運行的關鍵。

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

我們可以看到,ret參數有donevalue,那么ret肯定就是遍歷器每次next()的結果。如果發現遍歷器遍歷結束的話,便直接return整個大Promise的resolve(ret.value)方法結束遍歷。對了,此遍歷器的next()和co的next()在這里是不一樣的。當然你可以認為co將遍歷器的next()又封裝了一遍方便源碼使用。

接着看,如果並沒有完成遍歷。我們就會對ret.value調用toPromise(),這里有知識點延伸,暫且先跳過,因為我們 一個 promise化的異步操作就是返回promise的。不知道大家get到point沒?我就透漏一點,當是數組或對象時,co會識別並支持多異步的並行操作,先不管~~

我們在保證我們調用異步操作得到的value是promise后,我們就會調用value.then()方法為promise的onFulfilled()onRejected()進行回調的綁定。也就是說,這段時間程序都是在干其他和遍歷器無關的事的。遍歷器沒有得到遍歷器的next()指令,就一直靜靜的等着。我們可以想到,next()指令,必定是放在了那兩個回調函數(onFulfilledonRejected)里。

自動運行

promise化的異步API是先綁定了回調方法,然后等待異步完成后進行觸發。所以我們把遍歷器繼續遍歷的next()指令放在回調中,就可以達到回調返回數據后再調用遍歷器next()指令,遍歷器才會繼續下一個異步操作。

	function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res); // 遍歷器進行遍歷,ret是此次遍歷項
      } catch (e) {
        return reject(e);
      }
      next(ret); // ret.value is a promise
    }

我們看到第四行,通過調用遍歷器的next(res),再次啟動遍歷器得到新的遍歷結果,再傳入conext()里,重復之前的操作,達到自動運行的效果。這里需要注意一個地方,我們是通過向遍歷器的next(res)傳入res變量來實現將異步執行后的數據保存到遍歷器里。

理解的關鍵

我相信我不可能說的很明白,讓大家一下子就知道關鍵重點是哪個。我自己也是悟了不少時間的,最終發現那個可以使思路清晰的就是Deferred延遲對象。我在第二篇也有着重說過Deferred延遲對象,它最重要的一點就是,它是用來延遲觸發回調的。我們先通過延遲對象的promise進行回調的綁定,然后在Node的異步操作的回調中觸發promise綁定的函數,實現異步操作。當然這里也是如此,我們是把遍歷器的next()指令延遲到回調時再觸發。當然在co源碼里是直接使用了ES6的promise原生對象,我們看不到deferred的存在。

所以我很早前就說了,promise對理解co至關重要。之前在promise上也花費了特別大的精力去理解,並分析原理。所以大家如果沒有看之前的有關promise文章的,最好都回去看一看,絕對有好處!

co其他的內容


分析完co最關鍵的部分,接下來就是其他各種有用的源碼分析。關於thunk轉化為promise我就不說了,畢竟它也是被淘汰了的東西。那要說的東西其實就兩個,一個是多異步並行,一個是將co-generator轉化為常規函數。我們一個一個來講:

多異步並行

之前也有提到過,就是我們需要對迭代對象的值進行toPromise()操作。這個操作顧名思義,就是將所有需要yield的值,通通轉化為promise對象。它的源碼就是這樣的,並不能看到實質的東西:

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;
}

我們還記得在conext()函數里可以看到有一個注釋是這樣的:

'You may only yield a function, promise, generator, array, or object'

意思是,我們不僅僅只可以yield一個promise對象。function和promise我們就不說了,重點就是在array和object上,它們都是通過遞歸調用toPromise()來實現每一個並行操作都是promise化的。

數組Array

我們先看看相對簡單的array的源碼:

function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}

map是ES5的array的方法,這個相信也有人經常使用的。我們將數組里的每一項的值,再進行一次toPromise操作,然后得到全部都是promise對象的數組交給Promise.all方法使用。這個方法在promise文章的第二篇也講過它的實現,它會在所有異步都執行完后才會執行回調。最后resolve(res)res是一個存有所有異步操作執行完后的值的數組。

對象Object

Object就相對復雜些,不過原理依然是大同小異的,最后都是回歸到一個promise數組然后使用Promise.all()。使用Object的好處就是,異步操作的名字和值是可以對應起來的,來看看代碼:

function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj); // 得到的是一個存對象keys名字的數組
  var promises = [];           // 用於存放promise
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
  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[key] = res;
    }));
  }
}

第一個就是新建一個和傳入的對象一樣構造器的對象(這個寫法太厲害了)。我們先獲得了對象的所有的keys屬性名,然后根據keys,來獲取到每一個對象的屬性值。一樣是用toPromise()讓屬性值——也就是並行操作promise化,當然非promise的值就會直接存到results這個對象里。如果是promise,就會執行內部定義的defer(promise, key)函數。

所以理解defer函數是關鍵,我們看到是在defer函數里,我們才將當前的promise推入到promises數組里。並且每一個promise都是綁定了一個resolve()方法的,就是將結果保存到results的對象中。最后我們就得到一組都是promise的數組,通過Promise.all()方法進行異步並行操作,這樣每個promise的結果都會保存到result對象相應的key里。而我們需要進行數據操作的也就是那個對象里的數據。

這里強烈建議大家動手模擬實現一遍 objectToPromise。

co.wrap(*generatorFunc)

下一個很有用的東西就是co.wrap(),它允許我們將co-generator函數轉化成常規函數,我覺得這個還是需要舉例子來表明它的作用。假設我們有多個異步的讀取文件的操作,我們用co來實現。

//讀取文件1
co(function* (){
    var filename = yield readFile('hello1.txt', 'utf-8');
    return filename;
}).then(console.log, console.error);
//讀取文件2
co(function* (){
    var filename = yield readFile('hello2.txt', 'utf-8');
    return filename;
}).then(console.log, console.error);

天啊,我仿佛又回到了不會使用函數的年代,一個功能一段函數,不能復用。當然co.wrap()就是幫你解決這個問題的。

var getFile = co.wrap(function* (file){
    var filename = yield readFile(file, 'utf-8');
    return filename;
});

getFile('hello.txt').then(console.log);
getFile('hello2.txt').then(console.log);

例子很簡單,我們可以將co-generator里的變量抽取出來,形成一個常規的Promise函數(regular-function)。這樣子就無論是復用性還是代碼結構都是優化了不少。

既然知道了怎么用,就該看看它內部如何實現的啦,畢竟這是一次源碼分析。其實如果對函數柯里化(偏函數)比較了解,就會覺得非常簡單。

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn; // 這個應該是像函數constructor的東西
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

就是一個偏函數,借助於高階函數的特性,返回一個新函數createPromise(),然后傳給它的參數都會被導入到Generator函數中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM