從源碼理解Redux和Koa2的中間件機制

Redux和Koa的中間件機制相關源碼都很精簡。redux

正文我將直接引用部分源碼,並加以註釋來幫助咱們更清晰的理解中間件機制。api

Reudx

redux的中間件機制在源碼中主要涉及兩個模塊數組

內部的compose組合函數

'redux/src/compose.js'

//compose組合函數,接收一組函數參數返回一個組合函數
//須要提早注意的一點是,funcs數組內的函數基本上(被注入了api)就是咱們在將來添加的中間件如logger,thunk`等
export default function compose(...funcs) {
//爲了保證輸出的一致性,始終返回一個函數
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
 //這一步可能有些抽象,但代碼極其精緻,經過歸約函數處理數組,最終返回一個逐層自調用的組合函數。
 //例: compose(f, g, h) 返回 (...args) => f(g(h(...args))).
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

//或許是版本更新的緣故,相比以前看到過的compose要精簡了不少,尤爲是在最終的規約函數處理上,高大上了很多。
//由本來的reduce來依次執行中間件進化爲函數自調用,更加的【函數式】。。下面順便貼出多是舊的compose函數,你們自行對比。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}
複製代碼

咱們添加中間件時最經常使用到的redux提供的applyMiddleware函數

'redux/src/applyMiddleware.js'

//能夠看到compose函數被做爲🔧引入了
import compose from './compose'
//暴露給開發者,用來傳入中間件
export default function applyMiddleware(...middlewares) {
    //createStore函數的控制權將被轉讓給applyMiddleware,因爲本篇主要談中間件,就不擴展來解釋了
  return createStore => (...args) => {
//------------------------------------------------------------------非中間件相關,一些上下文環境的代碼-----------------------
    //初始化store,此處的...args實際爲reducer, preloadedState(可選)
    const store = createStore(...args)
    //聲明一個零時的dispatch函數,注意這裏的let,它將在構建完畢後被替換
    let dispatch = () => {
     throw new Error('dispatch不容許在構建中間件的時候被調用,其實主要是爲了防止用戶自定義的中間件在初始化的時候調用dispatch。 在下文的示例中能夠看到, 而且普通的同步的中間件通常是用不到dispatch的')
    }
    //提供給中間件函數的api,能夠看到dispatch函數在這裏經過函數來'動態的調用當前環境下的dispatch'
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    //爲中間件注入api
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
//------------------------------------------------------------------------------------------------------------------------
    
    '關鍵'
    //列出上文的例子可能比較直觀: 調用compose(f, g, h)返回(...args) => f(g(h(...args))).
    //1.調用compose函數,返回一個由多個/一箇中間件構成的組合函數
    //2.將store.dispatch做爲參數傳入組合函數中用來返回一個新的/包裝過的dispatch函數
    //'注意:這部分須要聯繫下文中的中間件源碼來對照着進行理解,因此讓咱們暫時把這裏加入腦內緩存'
    dispatch = compose(...chain)(store.dispatch)
    
    //返回一個store對象,在添加了中間件的狀況下,咱們實際最終獲取的store就是從這裏拿到的。
    return {
      ...store,
      dispatch
    }
  }
}
複製代碼

第三方中間件

原本不想寫這麼長來着,但但願更多你們可以更簡單的理解,就多貼了些源碼,畢竟代碼遠比文字更好理解,下面我用logger和thunk的源碼(簡化)來作承接上文的簡要分析。promise

'redux-logger'
//因爲logger源碼看起來好像有點複雜(懶得看),我就簡單實現了...不嚴謹請輕噴

一般來講redux的中間件主要分爲兩層。

//第一層,用於接受store提供的API,在傳給構建中間件以前就會被調用。
const logger = ({getState}) => {
    // 第二層,利用了函數(currying)柯理化將計算/運行延遲,請讓我用更多的註釋來幫咱們理清思路...
    // 仍是先列出上文的例子比較直觀【手動滑稽】:)
    // 例:compose(f, g, h)返回(...args) => f(g(h(...args))). 
    // 關聯上文:dispatch = compose(...chain)(store.dispatch) 代入例子 ((dispatch) => f(g(h(dispatch))(store.dispatch)
    // 能夠看到清晰看到,中間件被自右向左執行,store.dispatch做爲參數被傳入給最早執行(最右側)的中間件
    // 中間件的第二層被執行,返回一個'接受action做爲參數的函數',這個函數做爲調用下一個(本身左側)的中間件,依次執行至最左側,最終返回的一樣是一個'接受action的函數'
    // 最終咱們調用的dispatch實際上就是這個被最終返回的函數
    // '咱們的真實流程是 dispatch(包裝過的) => 中間件1 => 中間件2 => dispatch(store提供的) => 中間件2 => 中間件1 => 賦值(若是有返回的話)'
    // 果真仍是沒有解釋清楚,請拋開個人註釋,多看幾遍代碼
    return next => action => {
        console.log('action', action)
        console.log('pre state', getState())
        //next實質就是下一個(右側)中間件返回的閉包函數/當前中間件若是是最後一個或者惟一的,那麼next就是store提供的dispatch
        //next(action)函數調用棧繼續往下走,也就是調用下一個(右側)中間件,nextVal會接受返回的結果
        const nextVal = next(action)
        console.log('next state', getState())
        //將結果返回給上一個中間件(左側)或者是開發者(第一個中間件的狀況下)
        return nextVal
    }
}


'redux-thunk'
//這個是官方的源碼,異常精簡,這個函數支持了dispatch的異步操做,讓咱們來看看如何實現的。
//這裏就不復述上面的註釋了,只解釋下關於異步的支持。
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        //將dispatch函數的執行權限轉移給開發者,咱們一般在異步結束以後調用dispatch(此時是同步的)。
        //注意:在這裏咱們本來的中間件執行流程被中斷,並從新以同步的模式執行了一遍,'因此redux-thunk在中間件中的位置將會對其他中間件形成影響,例如logger中間件被執行了兩次什麼的...'
        // 另外一個要注意的是,這裏的dispatch函數其實是在構築中間件後被包裝的函數。
        return action(dispatch, getState, extraArgument);
    }
    //dispatch同步時,直接將控制權轉讓給下一個中間件。
    //dispatch異步時,在異步結束後調用的dispatch中,一樣將控制權轉讓給下一個中間件。
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

複製代碼

小結

最後讓咱們梳理總結一下Redux的中間件流程。緩存

  1. 首先是提供一個compsoe函數用來生成一個由多箇中間件構成的組合函數(存在自調用能力)
  2. 將store的API注入中間件
  3. 將store.dispath做爲參數傳遞給一個由compose函數構成的組合函數,返回一個包裝後的dispatch(也就是咱們真實使用的dispatch)
  4. (或者是0.)構築一個特定結構的的中間件,第一層用於注入API,第二層用來接受上一個中間件返回的一個接受action做爲參數的函數,而且自身一樣返回一個包含中間件具體操做的接受action做爲參數的函數
  5. 由中間件提供的dispatch被調用,中間件被依次調用,若是遇到提供了異步支持,那麼在異步狀況下,dispatch會先按照普通流程調用,當遇到redux-thunk或者redux-promise等函數時,會以同步的形式從新調用當前dispatch(中間件也會被從新調用一遍)

下面丟張費了九牛二虎之力畫的調用流程圖...隨意看看就好...bash

Koa2

感受基本沒多少朋友看到這裏了吧...但我仍是要寫完。 同上,先貼源碼讓代碼來告訴咱們真相閉包

在redux裏,中間件是做爲一個附加的功能存在的,但在koa裏中間件是它最主要但機制。app

koa的核心代碼被分散在多個獨立的庫中,首先來看中間件機制核心的compose函數

'koa-compose'

//注意:'在函數中始終返回Promise,是因爲koa2採用了async await語法糖形式'
//接受一箇中間件數組
function compose (middleware) {
    返回一個處理函數,在Request請求的最後被調用,並傳入請求的相關參數
  return function (context, next) {
    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
      //中間件被執行完畢了,直接返回一個Promise
      if (!fn) return Promise.resolve()
      try {
        //將下一個中間件的函數調用包裹在next中,返回給當前的中間件
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        //監聽錯誤,並拋出
        return Promise.reject(err)
      }
    }
  }
}

//相比較redux的中間件缺乏了幾分函數式的精緻,但我依舊寫不出相似精簡的代碼.jpg
複製代碼

koa本體

'koa/lib/application.js'

'104-115行的use函數(簡化)'

//這是koa暴露給咱們的use函數,相信大多數同窗都不陌生
  use(fn) {
    //很是明瞭,就是將中間件添加入middleware數組
    this.middleware.push(fn);
    return this;
  }

'125-136行的callback函數'
//callback函數將在koa.listen中被調用具體請自行查看源碼

  callback() {
    //調用compsoe
    const fn = compose(this.middleware);
    if (!this.listeners('error').length) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      //Request時被調用
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

複製代碼

koa2小結

關於koa2的中間件機制我並無解釋多少,主要是因爲相比redux中間件來講簡明許多,另外一個緣由主要是懶,具體的執行流程圖,實際上一樣是洋蔥形的,只是store.dispatch被換成了最後一箇中間件而已。koa

本篇文章,雖然質量不行,大多註釋偏口語化(專業詞彙量不足),但仍是但願可以對一些同窗有所幫助。異步

臨淵羨魚不如退而結網

相關文章
相關標籤/搜索