Co-實現原理分析

generator函數可以理解成一個異步操作的容器,它裝着一些異步操作,但並不會在實例化以後立即執行。而co的思想是在恰當的時候執行這些異步操作。那麼就需要一種機制,在一個異步操作執行完畢以後通知下一個異步操作開始執行。額,這句話聽起來就有點耳熟了。這不就是回調函數或者promise乾的事麼。確實,co要求generator裏yield的是thunk或者promise就是這個道理。thunk就是一種回調機制。
那麼co就有兩種實現方式,promise或者thunk。co4.0之前是用thunk實現的,之後是用promise實現的。

以下所有代碼可以在github上查看源碼。

基於thunk實現co

查看thunk函數
大致思路就是,在yield中調用thunk,thunk回調中調用genetator的next方法,直至next返回的對象{value, done}中done爲true。

function run(fn) {
//實例化generator,但因爲generator的特性,並沒有調用
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    //在調用下一次next之前,你可以加一些自己的操作
    console.log('--------next in thunk----------');
    if (result.done) return;
    //{value:next傳入的值,done:boolean},由於fn應爲一個yield thunk的generator,故value接受的其實是一個回調,這個回調在co(或者說本函數run中就是generator的next)
    result.value(next);
  }

  next();
}

調用

var readFile = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFile('./hi.txt');
  console.log(r1.toString());
  var r2 = yield readFile('./hello.txt');
  console.log(r2.toString());
};

console.log('------------------run thunk-----------------');
run(gen);

執行結果:
這裏寫圖片描述
thunkify是如何實現的請移步github查看,你也可以使用require的方式,nodejs有這個工具。

基於promise實現co

大致思路同thunk一樣,只是把回調的實現方式變成了promise,同理就需要genenrator中yield的是promise。

function run2(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();
} 

調用方式:

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

var gen2 = function* (){
  var f1 = yield readFile2('./hi.txt');
  console.log(f1.toString());
  var f2 = yield readFile2('./hello.txt');  
  console.log(f2.toString());
};

console.log('-----------------run2 promise-----------------');
run2(gen2);

執行結果:
這裏寫圖片描述

co源碼分析

基本原理就是以上所說。co就是對以上兩種方式進行擴展、封裝。

function co(gen) {
  var ctx = this;

  //如果是generatorFunction,就執行 獲得對應的generator對象
  if (typeof gen === 'function') gen = gen.call(this);

  //返回一個promise
  return new Promise(function(resolve, reject) {

    //初始化入口函數,第一次調用
    onFulfilled();

    //成功狀態下的回調
    function onFulfilled(res) {
      var ret;
      try {
        //拿到第一個yield返回的對象值ret
        ret = gen.next(res);
      } catch (e) {
        //出錯直接調用reject把promise置爲失敗狀態
        return reject(e);
      }
      //開啓調用鏈
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        //拋出錯誤,這邊使用generator對象throw。這個的好處是可以在co的generatorFunction裏面使用try捕獲到這個異常。
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }


    function next(ret) {
      //如果執行完成,直接調用resolve把promise置爲成功狀態
      if (ret.done) return resolve(ret.value);
      //把yield的值轉換成promise
      //支持 promise,generator,generatorFunction,array,object
      //toPromise的實現可以先不管,只要知道是轉換成promise就行了
      var value = toPromise.call(ctx, ret.value);

      //成功轉換就可以直接給新的promise添加onFulfilled, onRejected。當新的promise狀態變成結束態(成功或失敗)。就會調用對應的回調。整個next鏈路就執行下去了。
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);

      //否則說明有錯誤,調用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) + '"'));
    }
  });
}

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

在co4.0中,你依然可以yield thunk,因爲co會將其轉換爲promise,你可以yield object或者array,co也會將其轉換爲promise,且object或array的成員會同步開始執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章