Thunkify源碼,Co源碼以及與Koa的理解

Co源碼以及與Koa的深入理解

tj大神的co,將本來(lái)應(yīng)該是一個(gè)數(shù)據(jù)類(lèi)型的generator變成了一種處理異步的解決方案

其實(shí)主要就是一個(gè)遍歷函數(shù),將promise或者generator的異步函數(shù)一直執(zhí)行到得到最后的結(jié)果再返回,這樣就可以把本來(lái)放到異步中的方法按照同步的順序來(lái)寫(xiě)。

yield

函數(shù)內(nèi)部的yield后面?zhèn)魅氲目梢允且韵?/p>

  • Promise(就是promise嘛)
  • thunks(就是一個(gè)偏函數(shù),執(zhí)行之后只有一個(gè)簡(jiǎn)單的擁有一個(gè)callback的參數(shù)的函數(shù))
  • array(通過(guò)array可以并行執(zhí)行里面的function,并行是主要的價(jià)值)
  • objects(和array相同,也是并行執(zhí)行里面的yieldable,并行是主要的價(jià)值)
  • generators
  • generators functions(下面的這兩個(gè)東西可以支持,但是并不被推薦,因?yàn)槲覀儜?yīng)該轉(zhuǎn)向更加標(biāo)準(zhǔn)的promise)

API

co(fn*).then

將一個(gè)generator解決為一個(gè)promise

var fn = co.wrap(fn*)

講一個(gè)generator轉(zhuǎn)化為一個(gè)返回promise的常規(guī)函數(shù)

本質(zhì)的探索

他的最初實(shí)現(xiàn)是基于Thunk函數(shù)的。接收了一個(gè)生成器函數(shù)作為參數(shù),并生成了一個(gè)實(shí)際操作函數(shù),函數(shù)通過(guò)接收回調(diào)的方式來(lái)傳入最后的返回值。

所以先了解下thunk函數(shù)

這東西的發(fā)展是由函數(shù)的求值策略的分歧決定的,兩種求值策略

  var b = 1;
 function a(x,y){
   return y;
 }
 a(b+1);

上面的代碼一b+1在什么時(shí)候執(zhí)行比較好,

一種是傳值調(diào)用,在進(jìn)入函數(shù)體之前就直接執(zhí)行完,把值傳進(jìn)去。c語(yǔ)言是這么做的

一種是傳名調(diào)用,將表達(dá)式傳入函數(shù)體,只在用到他的時(shí)候求值。Hskell語(yǔ)言是這么做的

前一種會(huì)簡(jiǎn)單一些,但是會(huì)有性能損失,所以傾向于傳名調(diào)用。

傳名函數(shù)的編譯器實(shí)現(xiàn),其實(shí)就是放入一個(gè)臨時(shí)函數(shù),再將臨時(shí)函數(shù)傳入函數(shù)體,這個(gè)臨時(shí)函數(shù)就叫做thunk函數(shù)。

js語(yǔ)言是傳值調(diào)用,他的thunk含義有些不同,js中,thunk函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù),將它替換成單參數(shù)的版本,且只接受回調(diào)函數(shù)作為參數(shù)。

  //正常的readFile函數(shù)
  fs.readFile(fileName, callback);
  var readFileThunk = Thunk(fileName);
  readFileThunk(callback);
  //thunk版本的函數(shù)
  function Thunk(fileName){
    return function(callback){
      fs.readFile(fileName,callback);
    }
  }

所以其實(shí)任何有回調(diào)的函數(shù)都是可以搞成thunk形式的,下面是一個(gè)簡(jiǎn)單的生成器

  var Thunk = function(fn){
    return function () {
      //先傳入其他的參數(shù)初始化
      var args = Array.prototype.slice.call(arguments);
      //傳入callback返回的函數(shù)
      return function(callback){
        args.push(callback);
        //實(shí)際調(diào)用的時(shí)候
        return fn.apply(this,args);
      }
    }
  }
  var readFileThunk = Thunk(fs.readFile);
  readFileThunk(fileA)(callback);

tj的thunkify源碼

/**
 * Module dependencies.
 */
var assert = require('assert');
/**
 * Expose `thunkify()`.
 */
module.exports = thunkify;
/**
 * Wrap a regular callback `fn` as a thunk.
 *
 * @param {Function} fn
 * @return {Function}
 * @api public
 */
function thunkify(fn){
  assert('function' == typeof fn, 'function required');
  return function(){
    //這里就是將所有的參數(shù)放進(jìn)了一個(gè)新的數(shù)組,這里之所以不用[].slice。是因?yàn)橛腥嗽赽luebird docs發(fā)現(xiàn),如果直接這樣泄露arguments,v8的一些優(yōu)化的編譯會(huì)被擱置,就會(huì)有性能上的損失。
    var args = new Array(arguments.length);
    var ctx = this;
    for(var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }
    return function(done){
      //這里用called是為了標(biāo)記只執(zhí)行了一次,類(lèi)似于promise的resolve和reject只能執(zhí)行一次一樣。
      var called;
      args.push(function(){
        if (called) return;
        called = true;
        //因?yàn)閍rguments是一個(gè)list,必須得用apply才能在done傳入。
        done.apply(null, arguments);
      });
      //這里用個(gè)try catch,可以在執(zhí)行失敗時(shí)走一遍callback,傳入err信息
      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};
pic2.png
pic1.png

generator函數(shù)的回調(diào)流程管理

包裝成這樣到底有個(gè)啥用場(chǎng)?用在了generator的流程管理

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var gen = function* (){
  var r1 = yield readFile('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFile('/etc/shells');
  console.log(r2.toString());
};
var g = gen();
var r1 = g.next();
r1.value(function(err, data){
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function(err, data){
    if (err) throw err;
    g.next(data);
  });
});

就如同上面的,generator的執(zhí)行過(guò)程實(shí)際上是將同一個(gè)回調(diào)函數(shù),反復(fù)傳入next的value結(jié)果中。這樣我們就可以遞歸的來(lái)自動(dòng)完成這個(gè)過(guò)程了。于是據(jù)誕生了基于thunk函數(shù)的執(zhí)行器,也就是co了。

最簡(jiǎn)單的co

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

執(zhí)行器幫我們不停地調(diào)用傳入生成器的next函數(shù),如果done為true的時(shí)候,代表迭代完成,會(huì)將值傳給回調(diào)函數(shù)。

當(dāng)然前提是每一個(gè)一步函數(shù)都得是thunk函數(shù)的形式。

thunk并不是generator函數(shù)的自動(dòng)執(zhí)行的唯一方案。我們需要的其實(shí)只是一個(gè)機(jī)制,循環(huán)調(diào)用,并且交出和返回程序的執(zhí)行權(quán),thunk可以做到,promise也可以做到。

首先將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('f1.js');
  var f2 = yield readFile('f2.js');
  console.log(f1);
  console.log(f2);
}

然后手動(dòng)執(zhí)行下generator函數(shù)

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

寫(xiě)一個(gè)自動(dòng)執(zhí)行器

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

co的源碼

下面的是co源碼的逐行閱讀,先把參照的一些圖片列舉出來(lái)

  //array原生的slice
  var slice = Array.prototype.slice;
  //這里寫(xiě)的這么古怪就只是想在es6的模塊引入時(shí)更加舒服一些,參見(jiàn)下面的圖片3
  module.exports = co['default'] = co.co = co;
  //將傳入的generator函數(shù)包裝成一個(gè)返回promise的方法
  //這是一個(gè)獨(dú)立的方法,就是將傳入的函數(shù)包裝成了co執(zhí)行前的形式
  co.wrap = function (fn) {
    //存了一個(gè)指針指向原generator函數(shù)
    createPromise.__generatorFunction__ = fn;
    return createPromise;
    function createPromise() {
      //返回的方法調(diào)用就會(huì)直接執(zhí)行co。
      return co.call(this, fn.apply(this, arguments));
    }
  };
  //執(zhí)行g(shù)enerator或者generator函數(shù)然后返回一個(gè)promise
  function co(gen) {
    var ctx = this;
    var args = slice.call(arguments, 1)
    // 將所有的東西放到一個(gè)promise里面,來(lái)防止引起內(nèi)存泄露錯(cuò)誤的promise chaining。
    //tudo:看一下這個(gè)issue see https://github.com/tj/co/issues/180
    //參見(jiàn)下面的內(nèi)存泄露的研究
    //https://github.com/promises-aplus/promises-spec/issues/179 看的我好累,完全沒(méi)有看懂啊?。?!
    //總之不管怎樣,他是把傳進(jìn)來(lái)的東西包裝成了一個(gè)promise
    return new Promise(function(resolve, reject) {
      //這里是判斷下gen是不是函數(shù),generators function執(zhí)行之后是一個(gè)object
      if (typeof gen === 'function') gen = gen.apply(ctx, args);
      //傳入的不是generators函數(shù),沒(méi)有next,就直接resolve返回結(jié)果;這里是錯(cuò)誤兼容而已,因?yàn)閏o就是基于generator的,傳入其他的沒(méi)有意義
      if (!gen || typeof gen.next !== 'function') return resolve(gen);
      //主要就是走下面的onFulfilled方法,這個(gè)方法返回的是一個(gè)promise(resolve或者reject)
      onFulfilled();
      function onFulfilled(res) {
        var ret;
        try {
          //調(diào)用第一次next方法
          ret = gen.next(res);
        } catch (e) {
          //出錯(cuò)了直接reject出去
          return reject(e);
        }
        //將第一次的結(jié)果({done:true,value:{}})傳入內(nèi)部方法next
        next(ret);
      }
      //promise失敗的時(shí)候調(diào)用
      //這里在promise錯(cuò)誤的時(shí)候,就會(huì)嘗試向外throw err。Genertor的屬性,可以內(nèi)部拋出,外部不活。如果我們對(duì)這個(gè)yield進(jìn)行了try catch,就會(huì)被捕獲,不處理的話,就會(huì)reject出去,在co的catch語(yǔ)句中co(*fn).catch處理。
      function onRejected(err) {
        var ret;
        try {
          ret = gen.throw(err);
        } catch (e) {
          return reject(e);
        }
        next(ret);
      }
      //循環(huán)得到next的結(jié)果,return的還是一個(gè)promise
      function next(ret) {
        //如果done為true的話,代表執(zhí)行結(jié)束,返回一個(gè)resolve的promise
        if (ret.done) return resolve(ret.value);
        //既然還沒(méi)執(zhí)行完,就將ret.value轉(zhuǎn)換成一個(gè)promise
        var value = toPromise.call(ctx, ret.value);
        //如果成功轉(zhuǎn)化為了promise,就在這個(gè)promise執(zhí)行完了再調(diào)用onFulfilled方法
        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) + '"'));
      }
    });
  }
  //將yield后面的東西轉(zhuǎn)化成一個(gè)promise
  function toPromise(obj) {
    //如果不存在的話,直接返回,走最后的報(bào)錯(cuò)流程
    if (!obj) return obj;
    //判斷傳入的是不是promise,是的話直接返回
    if (isPromise(obj)) return obj;
    //判斷傳入的是不是generator,或者generator function,是的話,繼續(xù)調(diào)用co函數(shù)進(jìn)行循環(huán)~
    if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
    //如果就是個(gè)普通的thunk函數(shù),也把他轉(zhuǎn)化為promise
    if ('function' == typeof obj) return thunkToPromise.call(this, obj);
    //如果是array或者object的話,也走相應(yīng)地變換方法
    if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
    if (isObject(obj)) return objectToPromise.call(this, obj);
    //如果都不是,直接返回,走最后的報(bào)錯(cuò)流程
    return obj;
  }
  //這里將thunk轉(zhuǎn)化成了promise,thunk就是調(diào)用的時(shí)候傳入一個(gè)error和res的function,就在最外面包了個(gè)promise就行了
  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);
      });
    });
  }
  //這里的array轉(zhuǎn)化為promise其實(shí)就是通過(guò)Promise.all來(lái)包裹,這個(gè)方法只接受promise的數(shù)組,并且裝化為一個(gè)新的promise
  //參見(jiàn)下面的promise平行執(zhí)行的研究
  function arrayToPromise(obj) {
    return Promise.all(obj.map(toPromise, this));
  }
  //將一個(gè)object轉(zhuǎn)化為promise,其實(shí)就是內(nèi)部調(diào)用了promise.all方法而已
  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]);
      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;
      }));
    }
  }
  //檢查是否是promise,果然就是簡(jiǎn)單的判斷他有沒(méi)有then方法
  function isPromise(obj) {
    return 'function' == typeof obj.then;
  }
  //這里判斷是不是generator就是判斷他的next和throw方法是不是function
  function isGenerator(obj) {
    return 'function' == typeof obj.next && 'function' == typeof obj.throw;
  }
  //判斷是否是generatorFunction就是判斷了他的constructor的name
  function isGeneratorFunction(obj) {
    var constructor = obj.constructor;
    //這里是為了解決沒(méi)有constructor的對(duì)象,比如Object.create(null)
    if (!constructor) return false;
    //這里兩種情況會(huì)返回true,一種是名字正確地,一種是他的prototype是generator
    if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
    return isGenerator(constructor.prototype);
  }
   //就是通過(guò)constructor來(lái)判斷是不是一個(gè)簡(jiǎn)單的對(duì)象
  function isObject(val) {
    return Object == val.constructor;
  }

圖片3

pic3.png

promise chaining導(dǎo)致的內(nèi)存泄露

這里只是源碼的一個(gè)小注釋?zhuān)タ戳瞬簧贃|西

閱讀了https://github.com/tj/co/issues/180

有人發(fā)現(xiàn)在一個(gè)無(wú)限循環(huán)的for循環(huán)里面使用co調(diào)用一個(gè)異步操作,會(huì)發(fā)生內(nèi)存泄露

有人推斷是所有的promise都被連接了起來(lái),阻止了gc的回收

有人測(cè)試了基于thunk的v3,發(fā)現(xiàn)ok,v4發(fā)現(xiàn)內(nèi)存泄露,并且使用工具發(fā)現(xiàn)確實(shí)是promise的問(wèn)題

死馬說(shuō)這事規(guī)范里的問(wèn)題,bluebird和then/promise已經(jīng)做出了修復(fù),

最后hax說(shuō)是es6 spec “bug”

接下來(lái)看一個(gè)解決方案

就是用一個(gè)promise從外面包裹住全部,到底為啥還是沒(méi)看懂....跪求大神賜教

promise的平行執(zhí)行

promise被創(chuàng)建的時(shí)候就開(kāi)始了他們的任務(wù),是無(wú)法被執(zhí)行的。他們只代表了結(jié)果的狀態(tài),將他們傳給promise.all的時(shí)候甚至都是并行執(zhí)行的。他不關(guān)心執(zhí)行順序,也不關(guān)心是否平行運(yùn)行。

Co的錯(cuò)誤處理

這里主要是涉及到generator.throw方法,可以在generator外部拋出異常,而在generator內(nèi)部來(lái)catch住異常。

co里面使用了這個(gè)屬性,就可以針對(duì)某幾個(gè)yield進(jìn)行try catch,如果不進(jìn)行處理,統(tǒng)一的會(huì)在后面的catch語(yǔ)句中co(*fn).catch找到。

Koa執(zhí)行的理解

pic4.png

請(qǐng)求進(jìn)來(lái)的時(shí)候會(huì)一次經(jīng)過(guò)各個(gè)中間件進(jìn)行執(zhí)行,中間件之間的跳轉(zhuǎn)是yield next,執(zhí)行完了之后就會(huì)逆序執(zhí)行。

app.use(function *(next){
  var start = new Date;
  //執(zhí)行到這句話的時(shí)候跳到下一個(gè)中間件
  yield next;
  //下面的中間件執(zhí)行完了之后再執(zhí)行下面的部分
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

寫(xiě)兩個(gè)yield next會(huì)有什么問(wèn)題?

這里遇到y(tǒng)ield next其實(shí)還是會(huì)繼續(xù)向下執(zhí)行下一個(gè)generator的,但是因?yàn)橄乱粋€(gè)中間件done的狀態(tài)已經(jīng)是true了,再次調(diào)用一下此前已經(jīng)執(zhí)行完的generator,調(diào)用返回的結(jié)果肯定還是done為true,因?yàn)榇饲耙呀?jīng)執(zhí)行完了。所以后面繼續(xù)yield next是沒(méi)有意義的~~

Koa的中間件是運(yùn)行在co函數(shù)之下的。

Koa的中間件的實(shí)現(xiàn)

這里看到了一個(gè)Koa中間件的實(shí)現(xiàn)

var gens = [];
function use(generetor){
    gens.push(generetor);
}
// 實(shí)現(xiàn)co函數(shù)
function co(flow, isGenerator){
    var gen;
    if (isGenerator) {
        gen = flow;
    } else {
        gen = flow();
    }
    return new Promise(function(resolve){
        var next = function(data){
            var result = gen.next(data);
            var value = result.value;
            // 如果調(diào)用完畢,調(diào)用resolve
            if(result.done){
                resolve(value);
                return;
            }
            // 如果為yield后面接的為generator,傳入co進(jìn)行遞歸,并且將promise返回
            if (typeof value.next === "function" && typeof value.throw === "function") {
                value = co(value, true);
            }
            if(value.then){
                // 當(dāng)promise執(zhí)行完畢,調(diào)用next處理下一個(gè)yield
                value.then(function(data){
                    next(data);
                })
            }
        };
        next();
    });
}
function trigger(){
    var prev = null;
    var m = gens.length;
    co(function*(){
        while(m--){
            // 形成鏈?zhǔn)絞enerator
            prev = gens[m].call(null, prev);
        }
        // 執(zhí)行最外層generator方法
        yield prev;
    })
}
use(function*(next){
    var d = yield new Promise(function(resolve){
        setTimeout(function(){
            resolve("step1")
        }, 1000)
    });
    console.log(d);
    yield next;
    console.log("step2");
});
use(function*(next){
    console.log("step3");
    yield next;
    var d = yield new Promise(function(resolve){
        setTimeout(function(){
            resolve("step4")
        }, 1000)
    });
    console.log(d);
});
use(function*(){
    var d = yield new Promise(function(resolve){
        setTimeout(function(){
            resolve("step5")
        }, 1000)
    });
    console.log(d);
    console.log("step6");
});
trigger();

Koa的運(yùn)行順序圖

pic5.png

這張圖非常詳細(xì)了,原來(lái)本身的respond,以及自己定義的一些中間件統(tǒng)一的會(huì)被整成一個(gè)generator,然后交給co來(lái)執(zhí)行。

本文看了不少文章,引用的一些列出了原地址:

http://segmentfault.com/a/1190000002783230

http://www.ruanyifeng.com/blog/2015/05/thunk.html

http://www.cnblogs.com/axes/p/4683176.html

http://purplebamboo.github.io/2015/01/16/koa-source-analytics-4/

先寫(xiě)到這里,順便給個(gè)github的傳送門(mén),喜歡的朋友star一下啊,自己平時(shí)遇到的問(wèn)題以及一下學(xué)習(xí)的經(jīng)歷及所得都會(huì)在github上進(jìn)行記錄~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對(duì)JavaScript語(yǔ)言太重要。Javascript語(yǔ)言的執(zhí)行環(huán)境是“單線程”的,如果沒(méi)有異步編程,根本...
    呼呼哥閱讀 7,400評(píng)論 5 22
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,875評(píng)論 0 5
  • 特別說(shuō)明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 547評(píng)論 0 0
  • 陸陸續(xù)續(xù)用了koa和co也算差不多用了大半年了,大部分的場(chǎng)景都是在服務(wù)端使用koa來(lái)作為restful服務(wù)器用,使...
    Sunil閱讀 1,677評(píng)論 0 3
  • 前幾天翻開(kāi)到手的希臘文課本,閱讀完開(kāi)篇,倒吸了一口冷氣。在開(kāi)篇里作者向英文讀者(該希臘文課本是英文著的)透露了希臘...
    八月的風(fēng)思閱讀 306評(píng)論 0 0

友情鏈接更多精彩內(nèi)容