thunkify與co源碼解讀

開頭

首先本文有將近3000字,閱讀可能會佔用你20分鐘左右。

文筆可能不佳,但願能幫助到閱讀此文的人有一些收穫


在進行源碼閱讀前
首先抱有一個疑問,thunk函數是什麼,thunkify庫又是幹什麼的,co又是幹嗎,它有啥用node

程序語言有兩種求值策略

傳名調用 傳入參數其實是傳入函數體

傳值調用 函數體在進入的時候就進行運算計算值

編譯器的"傳名調用"實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk 函數。

在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數git

這幾句話來自阮一峯老師的blog文章


試想下咱們在node環境下要使用fs.readfilegithub

fs.readfile('filename',function(err,data){
     if(err){
          console.log(err)
          return
     }
})

而使用thunk簡單改造以後咱們的函數能夠變成這樣子的形式數組

var Thunk = function(filename){
    return function (callback){
        return fs.readfile(fileName,callback)
    }
}

此時調用readfile的話,咱們能夠這麼調用promise

var read = Thunk('filename')
read(callback);

thunkify出自tj大神之手網絡

thunkify源碼解析

var assert = require('assert');
module.exports = thunkify;
function thunkify(fn) {
    assert('function' == typeof fn, 'function required');
    // 引入斷言庫判斷是否是函數
    // 返回一個包含thunk函數的匿名函數
    return function () {
        var args = new Array(arguments.length);
        // 建立一個數組空間
        var ctx = this;
        // 獲取上下文環境用於後面綁定上下文

        for (var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }
        // 迭代傳參,由於有內存泄漏bug
        // 返回真正的thunk函數
        return function (done) {
            // done至關因而執行後的callback
            var called;
            // 聲明一個called保證只執行一次這個回調函數
            // 壓入一個數組中進行這種隔斷,防止被屢次執行
            args.push(function () {
                if (called) return;
                called = true;
                done.apply(null, arguments);
            });
            // 用try catch 在執行失敗也走一次callback 傳入err信息
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
};

代碼並不難懂
乍一看,這好像沒什麼用吧。併發

但 js後來有一個Generator函數,thunk此時彷彿有了做用app

Generator函數

使用yield 就是將控制權放出暫停執行
而後返回一個當前指針(遍歷器對象)異步

因此咱們是否須要有一種方法接受而且能夠繼續返回這種控制權
顯式的調用next當然沒有問題。可是咱們要自動的話?該怎麼辦async

基於自動流程管理,咱們利用thunk函數的特性,調用回調函數callback
回調函數裏面遞歸調用generator的next方法
直到狀態值爲done generator函數結束
這時候整個generator就能夠很優雅地被解決

而後咱們想象,這個流程thunk函數能夠幹什麼

主要的功能實際上是經過封裝多層使得咱們能夠在回調函數內得到控制權
返回控制權
由於通常按照正常寫法
咱們須要顯式地調用next next來使得咱們的Generator一步步完成
那麼咱們只須要一種機制,能夠幫助咱們得到控制權,而且返回控制權
均可以實現自動化

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFile('xxxfilename');
  console.log(r1.toString());
  var r2 = yield readFile(' xxxfilename ');
  console.log(r2.toString());
};

function run(fn) {
  var gen = fn();
  function next(err, data) {
  var result = gen.next(data);
  if (result.done) return;
  result.value(next);
  }
  next();
}
run(gen);

這是一個簡單的demo利用thunkify實現自動化generator

thunk函數回調調用next是一種方法
Pormise的then調用next 同時也是一種解決辦法
區別在於thunk可控(指的是在回調中咱們能夠可控執行),promise當即執行


co是什麼

Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.

基於Generator,使用promise,讓你用一種更好的方式書寫異步代碼

co的源碼也並很少
大概兩百行
https://github.com/tj/co/blob...
要讀懂co源碼建議還得看看promise規範與用法

co源碼解析

var slice = Array.prototype.slice;
module.exports = co['default'] = co.co = co;
co.wrap = function (fn) {
  //兼容有參數的generator函數
  //利用柯里化將generator轉換成普通函數
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};
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至關於將一切都包裹在promise裏面。使得咱們co返回的可使用promise的方法
  // co的返回值是Promise對象。爲何能夠then和catch的根源
  return new Promise(function(resolve, reject) {
    //作類型的判斷。
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    //Generator函數執行以後會是typeof會是對象。
    //默認執行調用一次Generator返回一個遍歷器對象Generator
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    //判斷類型 若是不符合  promise就進入resolved
    // 看看是否是Generator指針
    //傳入的不是Generators函數,沒有next,
    // 就直接resolve返回結果;這裏是錯誤兼容而已,由於co就是基於generator function的,傳入其餘的沒有意義

    //執行onFulfilled
    onFulfilled();
    //返回一個promise

    //onFulfilled幹了什麼。其實跟咱們以前的同樣,只是這裏涉及到了promise的狀態。若是出錯了。狀態返回是reject
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
        //初始化啓動一遍Generator next
      } catch (e) {
        return reject(e);
        //一有錯誤的話就拋出錯誤轉向rejected
      }
      // 初始化即將第一次yield的·值·傳給next
      next(ret);
      //將這個指針對象轉交next函數處理
      // 實現自動化的關鍵
      return null;
    }
    function onRejected(err) {
      //接受error錯誤
      var ret;
      //這塊其實就是處理整個流程的錯誤控制
      try {
        ret = gen.throw(err);
        //利用Generator throw錯誤給try catch捕獲
      } catch (e) {
        return reject(e);
        //使得Promise進入rejected
      }
      next(ret);
    }
    function next(ret) {
      //接受指針對象

      if (ret.done) return resolve(ret.value);
      //顯示對ret指針狀態作判斷,done爲true證實generator已經結束
      //此時進入resolved結束整個Generator
      var value = toPromise.call(ctx, ret.value);
      //將yield 的值進行Promise轉換

      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      //value在咱們容許的範圍內,那麼value.then注入onFulfilled與onRejected,來執行下一次gen.next。
      //在onFulfilled又將調用next從而使得next不停的利用then作調用
      //若是值是存在而且能夠進行promise的轉換。(也就是否是基本類型/或假值)
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
      //若是沒有通過值轉換或者value爲空的時候。此時將拋出錯誤。
      //由於那就是所謂的基本類型不支持了
      //function, promise, generator, array, or object只支持這幾種的
    }
  });
}
//注意咱們就只容許這幾種類型轉換。
//那麼進入判斷的時候咱們就能夠很簡單地判斷了,而後決定promise的狀態
function toPromise(obj) {
  if (!obj) return obj;
  //若是obj undefined 或者別的假值返回這個undefined
  if (isPromise(obj)) return obj;
  //若是是個Promise的話就返回這個值
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  //判斷是否是Generator function是的話用co處理
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  //若是是函數的話,使用thunk to promise轉換
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  //若是是數組 使用array to promise
  if (isObject(obj)) return objectToPromise.call(this, obj);
  //若是是對象 使用object to promise 轉換
  return obj;
  //若是都不是 就返回`值`
}
// co關於yield後邊的值也是有必定的要求的,只能是一個 Function|Promise|Generator|Generator Function | Array | Object;
// 而 yield Array和Object中的item也必須是  Function|Promise|Generator | Array | Object;
// 若是不符合的話就將Promise rejected掉併發出警告

//下面是一些工具函數

//使用thunk後的fnction 咱們只容許它有一個參數callbak
//容許有多個參數 第一個參數爲error
//在node環境下 第一個爲error對象
function thunkToPromise(fn) {
  var ctx = this;
  return new Promise(function (resolve, reject) {
    fn.call(ctx, function (err, res) {
      if (err) return reject(err);
      if (arguments.length > 2) res = slice.call(arguments, 1);
      resolve(res);
    });
  });
}
// thunkToPromise傳入一個thunk函數
// 函數返回一個Promise對象
// promise裏面執行這個函數
// nodejs的回調函數 第一個參數都是err
// 若是有錯誤就進入rejected(前面咱們能夠看到 value.then(onFulfilled, onRejected); )
// 若是有error就rejected了
// 若是沒有的話就調用resolve( 後面onFulfilled )


//將數組中的全部值均promise化後執行,Promise.all會等待數組內全部promise均fulfilled、或者有一個rejected,纔會執行其後的then。
//對一些基本類型 例如數字 字符串之類的,是不會被toPromise轉換的
//最後在resolve(res)的時候 res就是存有全部異步操做執行完的值數組
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
//對象經過key進行遍歷,
//對於每一個被promise化好的value
//都將其存儲於promises中,最後Promise.all,
//生成results。
//objectToPromise實現實在是太可怕了=-=
//因此不少字企圖把它講順了
function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj);
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    // 確保obj[key]爲promise對象
    // 而後調用defer推入 promises等待value的promise resolved以後將key放入results
    // 不然直接將 results[key] = obj[key](也就是無須promise化的)
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
// 利用promise.all來使用異步並行調用咱們的promises
// 若是執行後進入resolved而後壓入results對象
// 最後固然是返回這個results對象
// 而後後面的then在得到時候 onFulfilled onRejected的參數將是這個results
// 這樣子咱們每一個promise的結果都會存在result對象對應的key內
// 返回的是一個promise 後面也就能夠接着.then(onFulfilled)
  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;
    }));
  }
}

//檢查是否是promise
//·鴨子類型·判斷。
function isPromise(obj) {
  return 'function' == typeof obj.then;
}
//判斷是否是Generator迭代器
function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
 //判斷是否是generator函數
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}
//判斷是否是對象
//plain object是指用JSON形式定義的普通對象或者new Object()建立的簡單對象
function isObject(val) {
  return Object == val.constructor;
}

co大概就是乾的,將generator自動化,更好的將異步流轉換同步寫法

ES7的async await
其實就是generator的語法糖 再加上一個內置自動執行的混合體
也就是究極體
await的返回值是一個promise

是否是很像co包裹的generator

參考內容:
阮一峯網絡日誌
co 源碼分析 co 與 co.wrap
co 源碼分析


結語

  1. 有兩種方法可使Generator自動化,thunk與Promise
  2. Generator自動化可使得咱們的異步代碼編寫得更像同步代碼(回調地獄是在太可怕了)
  3. 涉及異步的,現在不少都是利用Promise,因此掌握Promise是很重要的

但願閱讀此文的人能夠有一些收穫。若是有什麼錯誤的地方也但願能夠談出來或者私信我,一塊兒探討。渴望成長。^v^

相關文章
相關標籤/搜索