詳解redux中間件

關於redux中間件是什麼以及爲何須要redux中間件的話題,網上有太多的文章已經介紹過了,本文就再也不贅述了。若是你有相似的困惑:react

  • redux中間件到底是如何做用於dispatch?
  • redux的源碼和中間件的源碼都不復雜,但看起來怎麼那麼費勁?
  • redux中間件的洋蔥模型究竟是什麼?
  • ...

那麼歡迎往下閱讀,但願這篇文章能幫助你多一些對redux中間件的理解。git

在深刻理解中間件以前,咱們先來看一個很關鍵的概念。es6

複合函數/函數組合(function composition)

在數學中, 複合函數是指 逐點地把一個 函數做用於另外一個函數的結果,所獲得的第三個函數。

直觀地說,複合兩個函數是把兩個函數連接在一塊兒的過程,內函數的輸出就是外函數的輸入。github

-- 維基百科編程

你們看到複合函數應該不陌生,由於上學時的數學課本上都出現過,咱們舉例回憶下:redux

f(x) = x^2 + 3x + 1
g(x) = 2x

(f ∘ g)(x) = f(g(x)) = f(2x) = 4x^2 + 6x + 1

其實編程上的複合函數和數學上的概念很類似:數組

var greet = function(x) { return `Hello, ${ x }` };
var emote = function(x) { return `${x} :)` };
var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  }
}
var happyGreeting = compose(greet, emote);
// happyGreeting(「Mark」) -> Hello, Mark :)

這段代碼應該不難理解,接下來咱們來看下compose方法的es6寫法,效果是等價的:閉包

const compose = (...funcs) => {
  return funcs.reduce((f, g) => (x) => f(g(x)));
}

這個寫法可能須要你花點時間去理解。若是理解了,那麼恭喜你,由於redux的compose寫法基本就是這樣。可是若是一會兒沒法理解也不要緊,咱們只要先記住:app

  1. compose(A, B, C)的返回值是:(arg)=>A(B(C(arg))),
  2. 內函數的輸出就是外函數的輸入

咱們再舉個例子來理解下compose的做用:框架

// redux compose.js
function compose (...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

function console1(nextConsole) {
  return (message) => {
    console.log('console1開始');
    nextConsole(message);
    console.log('console1結束');
  }
}

function console2(nextConsole) {
  return (message) => {
    console.log('console2開始');
    nextConsole(message);
    console.log('console2結束');
  }
}

function console3(nextConsole) {
  return (message) => {
    console.log('console3開始');
    nextConsole(message);
    console.log('console3結束');
  }
}

const log = compose(console1, console2, console3)(console.log);

log('我是Log');

/* 
console1開始
console2開始
console3開始
我是Log
console3結束
console2結束
console1結束
*/

看到這樣的輸出結果是否是有點意外?咱們來進一步解析下:

由於:

compose(A, B, C)的返回值是:(arg) => A(B(C(arg)))

因此:

compose(console1, console2, console3)(console.log)的結果是:console1(console2(console3(console.log)))

由於:

內函數的輸出就是外函數的輸入

因此,根據console1(console2(console3(console.log)))從內到外的執行順序可得出:

console3的nextConsole參數是console.log

console2的nextConsole參數是console3(console.log)的返回值

console1的nextConsole參數是console2(console3(console.log))的返回值

也就是說在console1(console2(console3(console.log))執行後,因爲閉包的造成,因此每一個console函數內部的nextConsole保持着對下一個console函數返回值的引用。

因此執行log('我是Log')的運行過程是:

  1. 執行console1返回的函數,輸出「console1開始」,而後執行console1內部的nextConsole(message)時,會將引用的console2返回值推入執行棧開始執行。
  2. 因而輸出「console2開始」,而後執行console2內部的nextConsole(message)時,會將引用的console3返回值推入執行棧開始執行。
  3. 因而輸出「console3開始」,而後執行console3內部的nextConsole(message)時,發現nextConsole就是console.log方法,因而輸出「我是log」,接着執行下一句,輸出「console3結束」。執行完畢將console3函數推出執行棧。
  4. 此時執行棧頂部是console2函數,執行完console2的最後一條語句,輸出「console2結束」後,將console2函數推出執行棧。
  5. 同上,此時執行棧頂部是console1函數,執行完console1的最後一條語句,輸出「console1結束」後,將console1函數推出執行棧。

圖示:(和真實的執行棧會有差別,這裏做爲輔助理解)

image.png

(點擊查看大圖)

至此,整個運行過程就結束了。其實這就是網上不少文章裏提到的洋蔥模型,這裏我是以執行過程當中進棧出棧的方式來說解,不知道理解起來會不會更方便些~

關於複合函數就先介紹這些,篇幅有點長,主要是由於它在redux中間件裏起到了關鍵的做用。若是一下沒理解,能夠稍微再花點時間琢磨下,不着急往下讀,由於理解了複合函數,基本也就理解了redux中間件的大部分核心內容了。

解析applyMiddleware.js

接下來就是解讀源碼的時間了~

//redux applyMiddleware.js

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

首先來看下applyMiddleware的框架:applyMiddleware接受一箇中間件數組,返回一個參數爲createStore的函數,該函數再返回一個參數爲reducer、preloadedState、enhancer的函數。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {...}
}

這裏有兩個問題?

  1. 這些參數是從哪兒傳來的?
  2. 爲何要用柯里化的方式去寫?

先看第一個問題,是由於實際在configure store時,applyMiddleware是做爲redux createStore方法中第三個參數enhancer被調用:

// index.js
const store = createStore(reducer, initialState, applyMiddleware(...middlewares));


// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
   return enhancer(createStore)(reducer, preloadedState)
  }
  ...
}

咱們能夠在createStore的源碼中看到,當enhancer是function時,會先傳入自身createStore函數,返回的函數再傳入初始傳給createStore的reducer和preloadedState,因此第一個問題獲得瞭解答。而第二個問題是由於若是要給createStore傳多個enhancer的話,須要先用compose組合一下enhancer,而柯里化和compose的配合很是好用,因此這裏會採起柯里化的寫法。那爲何好用呢?之後會寫篇相關的文章來介紹,這裏先很少作介紹了~

咱們接着分析,那麼此時的enhancer是什麼?很明顯,就是applyMiddleware(...middlewares)的返回值

// applyMiddleware(...middlewares)
(createStore) => (reducer, preloadedState, enhancer) => {...}

那 enhancer(createStore)(reducer, preloadedState) 連續調用的結果是什麼?這就來到了applyMiddleware的內部實現,總得來講就是接收外部傳入的createStore、reducer、preloadedState參數,用createStore生成一個新的store對象,對新store對象中的dispatch方法用中間件加強,返回該store對象。

//  export default function applyMiddleware(...middlewares) 
//    return (createStore) => (reducer, preloadedState, enhancer) => {

        const store = createStore(reducer, preloadedState, enhancer)
        let dispatch = store.dispatch
        let chain = []

        const middlewareAPI = {
          getState: store.getState,
          dispatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
          ...store,
          dispatch // 返回給全局store的是通過中間件加強的dispatch
        }                                                                             
//    }
// }

接着咱們分析下內部實現,首先用dispatch變量保存store.dispatch,而後將getState方法和dispatch方法傳遞給中間件,這裏又有兩個問題:

  1. 爲何要將getState和dispatch傳給中間件呢?
  2. 爲何傳入的dispatch要用匿名函數包裹下,而不是直接傳入store.dispatch?
let dispatch = store.dispatch;
let chain = [];

const middlewareAPI = {
  getState: store.getState, 
  dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
  ...store,
  dispatch // 返回給全局store的是通過中間件加強的dispatch
}

關於第一個問題,咱們先來看兩個常見的中間件內部實現(簡易版)

// redux-thunk
function createThunkMiddleware ({ dispatch, getState }) {
  return (next) => 
      (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }

      return next(action);
    };
}

// redux-logger
function createLoggerMiddleware({ getState }) {
  return (next) => 
    (action) => {
      const prevState = getState();
      next(action);
      const nextState = getState();
      console.log(`%c prev state`, `color: #9E9E9E`, prevState);
      console.log(`%c action`, `color: #03A9F4`, action);
      console.log(`%c next state`, `color: #4CAF50`, nextState);
    };
}

其實第一個問題的答案也就有了,由於中間件須要接收getState和dispatch在內部使用,logger須要getState方法來獲取當前的state並打印,thunk須要接收dispatch方法在內部進行再次派發,

關於第二個問題咱們一會再解答 :)

咱們繼續分析源碼,那麼此時map後的chain數組也就是每一箇中間件調用了一次後的結果:

chain = [(next)=>(action)=>{...}, (next)=>(action)=>{...}, (next)=>(action)=>{...}];
// 要注意此時每一箇中間件的內部實現{...}都閉包引用着傳入的getState和dispatch方法

看到這裏是否是以爲很熟悉了?

// console1,console2,console3

(nextConsole) => (message) => {...}

const log = compose(console1, console2, console3)(console.log);

log('我是Log');

// log執行後輸出的洋蔥式結果不重複展現了

咱們一樣能夠推導出:

// middleware1, middleware2, middleware3
// (next) => (action) => {...}

// dispatch = compose(...chain)(store.dispatch); 等於下一行
dispatch = compose(middleware1, middleware2, middleware3)(store.dispatch);

若是調用dispatch(action),也會像洋蔥模型那樣通過每個中間件,從而實現每一箇中間件的功能,而該dispatch也正是全局store的dispatch方法,因此咱們在項目中使用dispatch時,使用的也都是加強過的dispatch。

至此咱們也瞭解了applyMiddleware是如何將中間件做用於原始dispatch的。

別忘了,咱們還漏了一個問題沒解答:爲何傳入的dispatch要用匿名函數包裹下,而不是直接傳入store.dispatch?

咱們再來看下內部實現:

let dispatch = store.dispatch // 1

const middlewareAPI = {
  getState: store.getState, 
  dispatch: (action) => dispatch(action) // 2
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 3

首先,代碼中三處的dispatch都是同一個,那麼經由匿名函數包裹的dispatch,經過middlewareAPI傳入middleware後,middleware內部的dispatch就能夠始終保持着對外部dispatch的引用(由於造成了閉包)。也就是說,當註釋3的代碼執行後,middleware內部的dispatch也就變成了加強型dispatch。那麼這樣處理有什麼好處呢?咱們來看個場景

// redux-thunk
function createThunkMiddleware ({ dispatch, getState }) {
  return (next) => 
      (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }

      return next(action);
    };
}

// 使用到thunk的異步action場景
const setDataAsync = () => {
  return (dispatch) => {
    setTimeout(() => {
        dispatch({ type: 'xxx', payload: 'xxx' });
    }, 3000)
  }
}

const getData = () => {
  return (dispatch) => {
    return fetch.get(...).then(() => { dispatch(setDataAsync()); })
  }
}

dispatch(getData());

若是是一個異步action嵌套另外一個異步action的場景,而此時傳入的dispatch若是是原始store.dispatch,dispatch(setDataAsync())的執行就會有問題,由於原始的store.dispatch沒法處理傳入函數的狀況,那麼這個場景就須要中間件加強後的dispatch來處理。

因此這也就解釋了爲何傳入的dispatch要用匿名函數包裹,由於可能在某些中間件內部須要使用到加強後的dispatch,用於處理更多複雜的場景。


好,關於redux中間件的內容就先介紹到這裏。很是感謝能看到此處的讀者,在如今碎片化閱讀盛行的時代,能耐心看完如此篇幅的文章實屬不易~

最後,打個小廣告,歡迎star一波我司自研的react移動端組件——Zarm

相關介紹文章:

對不起,咱們來晚了 —— 基於 React 的組件庫 Zarm 2.0 發佈

參考:

圖解Redux中middleware的洋蔥模型

Understanding Redux Middleware

相關文章
相關標籤/搜索