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

異步編程系列教程:git

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

如何使用co


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

// 注意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)一次:json

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

若是不清楚咱們上面說過的Generator遍歷器或promise對象的,能夠先放一放這篇文章,從以前的幾篇看起。api

進入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);
    
    //...下面代碼暫時省略...
   })
}

這裏咱們須要關注的有兩點:promise

  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()又封裝了一遍方便源碼使用。app

接着看,若是並無完成遍歷。咱們就會對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函數中。

相關文章
相關標籤/搜索