經過實例分析javascript中的「中間件」

介紹

若是你使用過redux或者nodejs,那麼你對「中間件」這個詞必定不會感到陌生,若是沒用過這些也不要緊,也能夠經過這個來了解javascript中的事件流程。javascript

一個例子

有一類人,很是的懶(好比說我),只有三種行爲動做,sleep,eat,sleepFirst,僞代碼就是:php

var wang = new LazyMan('王大錘');
wang.eat('蘋果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同於如下的代碼
const wang = new LazyMan('王大錘');
wang.eat('蘋果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);

執行結果以下圖:java

睡2S
無論什麼,先睡2Snode


圖片描述
而後作個介紹,吃東西,睡5Sgit


圖片描述
醒來,吃github


可是javascript只有一個線程,也並無像php的sleep的那種方法。實現的思路就是eat、sleep、sleepFirst這些事件放在任務列中,經過next去依次執行方法。我仍是但願在看源碼前先手動實現一下試試看,其實這就是個lazyMan的實現。編程


下面是個人實現方式:redux

class lazyMan{
    constructor(name) {
        this.tasks = [];
        const first = () => {
            console.log(`my name is ${name}`);
            this.next();
        }
        this.tasks.push(first);
        setTimeout(()=>this.next(), 0);
    }
    next() {
        const task = this.tasks.shift();
        task && task();
    }
    eat(food) {
        const eat = () => {
            console.log(`eat ${food}`);
            this.next();
        };
        this.tasks.push(eat);
        return this;
    }
    sleep(time) {
        const newTime = time * 1000;
        const sleep = () => {
            console.log(`sleep ${time}s!`);
            setTimeout(() => {
                this.next();
            }, newTime);
        };
        this.tasks.push(sleep);
        return this;
    }
    sleepFirst(time) {
        const newTime = time * 1000;
        const sleepzFirst = () => {
            console.log(`sleep ${time}s first!`);
            setTimeout(() => {
                this.next();
            }, newTime);
        };
        this.tasks.unshift(sleepzFirst);
        return this;
    }
}
const aLazy = new lazyMan('王大錘');
aLazy.eat('蘋果').eat('香蕉').sleep(5).eat('葡萄').eat('橘子').sleepFirst(2)

咱們上面說過api

wang.eat('蘋果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同於如下的代碼
wang.eat('蘋果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);

若是你使用過過node,你會發現,這種寫法彷佛有點熟悉的感受,咱們來看一下一個koa2(一個node的框架)項目的主文件:數組

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('koa-cors2');

const routers = require('./src/routers/index')

const app = new Koa();

app.use(cors());
app.use(bodyParser());
app.use(routers.routes()).use(routers.allowedMethods())

app.listen(3000);

有沒有發現結構有一點像?

koa中的中間件

廢話很少說,直接看源碼...
app.use就是用來註冊中間件的,咱們先看use的實現:

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

先解釋一下里面作了什麼處理,fn就是傳入的函數,首先確定要判斷是不是個函數,若是不是,拋出錯誤,其次是判斷fn是不是一個GeneratorFunction,我用的是koa2,koa2中用asyncawait來替代koa1中的generator,若是判斷是生成器函數,證實使用或者書寫的中間件爲koa1的,koa2中提供了庫koa-convert來幫你把koa1中的中間件轉換爲koa2中的中間件,這裏若是判斷出是koa1的中間件會給你提醒,這裏會主動幫你轉換,就是代碼中的convert方法。若是驗證沒出現問題,就註冊這個中間件並放到中間件數組中。
這裏咱們只看到了把中間件加到數組中,而後就沒有作其餘處理了。
咱們再看koa2中listen

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

這裏只是啓動了個server,而後傳進了一個回調函數的結果,咱們看原生啓動一個server大概是什麼樣的:

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(3000);

原生的回調函數接受兩個參數,一個是request一個是response,咱們再去看koa2中這個回調函數的代碼:

callback() {
    const fn = compose(this.middleware);

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

    const handleRequest = (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

這裏有一個const fn = compose(this.middleware);compose這種不知道你們用的多很少,compose是函數式編程中使用比較多的東西,這裏將多箇中間件組合起來。
咱們去看compose的實現:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

首先判斷是不是中間件數組,這個不用多說,for...of是ES6中的新特性,這裏不作說明,須要注意的是,數組和Set集合默認的迭代器是values()方法,Map默認的是entries()方法。

這裏的dispatch和next同樣是全部的中間件的核心,dispatch的參數i其實也就是對應中間件的下標,,在第一次調用的時候傳入了參數0,若是中間件存在返回Promise

return Promise.resolve(fn(context, function next () {
  return dispatch(i + 1)
}))

咱們lazyMan鏈式調用時不斷的shift()取出下一個要執行的事件函數,koa2裏採用的是經過數組下標的方式找到下一個中間件,這裏是用Promise.resolve包起來就達到了每個中間件await next()返回的結果都恰好是下一個中間件的執行。不難看出此處dispatch是個遞歸調用,多箇中間件會造成一個棧結構。其中i的值老是比上一次傳進來的大,正常執行index的值永遠小於i,但只要在同一個中間件中next執行兩次以上,index的值就會等於i,同時會拋出錯誤。但若是不執行next,中間件的處理也會終止。

整理下流程:

  1. compose(this.middleware)(ctx)默認會執行中間件數組中的第一個,也就是代碼中的dispatch(0),第一個中間件經過await next()返回的是第二個中間件的執行。
  2. 而後第二個中間件中執行await next(),而後返回第三個...以此類推
  3. 中間件所有處理結束之後,剩下的就是經過中間件中不斷傳遞的context來對請求做處理了。
相關文章
相關標籤/搜索