KOA 與 CO 實現淺析

KOA 與 CO 的實現都很是的短小精悍,只須要花費很短的時間就能夠將源代碼通讀一遍。如下是一些淺要的分析。node

如何用 node 實現一個 web 服務器

既然 KOA 實現了 web 服務器,那咱們就先從最原始的 web 服務器的實現方式着手。
下面的代碼中咱們建立了一個始終返回請求路徑的 web 服務器。web

const http = require('http');
const server = http.createServer((req, res) => {
  res.end(req.url);
});
server.listen(8001);

當你請求 http://localhost:8001/some/url 的時候,獲得的響應就是 /some/url數組

KOA 的實現

簡單的說,KOA 就是對上面這段代碼的封裝。promise

首先看下 KOA 的大概目錄結構:服務器

lib 目錄下只有四個文件,其中 request.jsresponse.js 是對 node 原生的 request(req)response(res) 的加強,提供了不少便利的方法,context.js 就是著名的上下文。咱們暫時拋開這三個文件的細節,先看下主文件 application.js 的實現。app

先關注兩個函數:dom

// 構造函數    
function Application() {
  if (!(this instanceof Application)) return new Application;
  this.env = process.env.NODE_ENV || 'development';
  this.subdomainOffset = 2;
  this.middleware = [];
  this.proxy = false;
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
}  
// listen 方法   
app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

上面的這兩個函數,正是完成了一個 web 服務器的創建過程:koa

const server = new KOA();  // new Application()
server.listen(8001);

而先前 http.createServer() 的那個回調函數則被替換成了 app.callback 的返回值。函數

咱們細看下 app.callback 的具體實現:oop

app.callback = function(){
  if (this.experimental) {
    console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
  }
  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
  var self = this;

  if (!this.listeners('error').length) this.on('error', this.onerror);

  return function handleRequest(req, res){
    res.statusCode = 404;
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn.call(ctx).then(function handleResponse() {
      respond.call(ctx);
    }).catch(ctx.onerror);
  }
};

先跳過 ES7 的實驗功能以及錯誤處理,app.callback 中主要作了以下幾件事情:

  • 從新組合中間件並用 co 包裝
  • 返回處理request的回調函數

每當服務器接收到請求時,作以下處理:

  • 初始化上下文
  • 調用以前 co.wrap 返回的函數,並作必要的錯誤處理

如今咱們把目光集中到這三行代碼中:

// 中間件重組與 co 包裝  
var fn = co.wrap(compose(this.middleware));
// ------------------------------------------  
// 在處理 request 的回調函數中  
// 建立每次請求的上下文  
var ctx = self.createContext(req, res);  
// 調用 co 包裝的函數,執行中間件  
fn.call(ctx).then(function handleResponse() {
  respond.call(ctx);
}).catch(ctx.onerror);

先看第一行代碼,compose 實際上就是 koa-compose,實現以下:

function compose(middleware){
  return function *(next){
    if (!next) next = noop();
    var i = middleware.length;
    while (i--) {
      next = middleware[i].call(this, next);
    }
    return yield *next;
  }
}
function *noop(){}

compose 返回一個 generator函數,這個 generator函數 中倒序依次以 next 爲參數調用每一箇中間件,並將返回的generator實例 從新賦值給 next,最終將 next返回。

這裏比較有趣也比較關鍵的一點是:

next = middleware[i].call(this, next);

咱們知道,調用 generator函數 返回 generator實例,當 generator函數 中調用其餘的 generator函數 的時候,須要經過 yield *genFunc() 顯式調用另外一個 generator函數

舉個例子:

const genFunc1 = function* () {
  yield 1;
  yield *genFunc2();
  yield 4;
}
const genFunc2 = function* () {
  yield 2;
  yield 3;
}
for (let d of genFunc1()) {
  console.log(d);
}

執行的結果是在控制檯依次打印 1,2,3,4。

回到上面的 compose 函數,其實它就是完成上面例子中的 genFunc1 調用 genFunc2 的事情。而 next 的做用就是保存並傳遞下一個中間件函數返回的 generator實例

參考一下 KOA 中間件的寫法以幫助理解:

function* (next) {
  // do sth.
  yield next;
  // do sth.
}

經過 compose 函數,KOA 把中間件所有級聯了起來,造成了一個 generator 鏈。下一步就是完成上面例子中的 for-of循環的事情了,而這正是 co 的工做。

co 的原理分析

仍是先看下 co.wrap

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

該函數返回一個函數 createPromise,也就是 KOA 源碼裏面的 fn
當調用這個函數的時候,實際上調用的是 co,只是將上下文 ctx 做爲 this 傳遞了進來。

如今分析下 co的代碼:

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)
  // 返回一個 promise
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    
    onFulfilled();
    
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      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) + '"'));
    }
  });
}

co 函數的參數是 gen,就是以前 compose 函數返回的 generator實例

co 返回的 Promise 中,定義了三個函數 onFulfilledonRejectednext,先看下 next 的定義。

next 的參數實際上就是gen每次 gen.next() 的返回值。若是 gen 已經執行結束,那麼 Promise 將返回;不然,將 ret.value promise 化,並再次調用 onFulfilledonRejected 函數。

onFulfilledonRejected 幫助咱們推動 gen 的執行。

nextonFulfilledonRejected 的組合,實現了 generator 的遞歸調用。那麼到底是如何實現的呢?關鍵還要看 toPromise 的實現。

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

toPromise 函數中,後三個分支處理分別對 thunk 函數、數組和對象進行了處理,此處略去細節,只須要知道最終都調回了 toPromise 的前三個分支處理中。這個函數最終返回一個 promise 對象,這個對象的 resolvereject 處理函數又分別是上一個 promise 中定義的 onFulfilledonRejected 函數。至此,就完成了 compose 函數返回的 generator 鏈的推動工做。

最後還有一個問題須要明確一下,那就是 KOA 中的 context 是如何傳遞的。
經過觀察前面的代碼不難發現,每次關鍵節點的函數調用都是使用的 xxxFunc.call(ctx) 的方式,這也正是爲何咱們能夠在中間件中直接經過 this 訪問 context 的緣由。

相關文章
相關標籤/搜索