Redux 中間件與函數式編程

爲何須要中間件

接觸過 Express 的同窗對「中間件」這個名詞應該並不陌生。在 Express 中,中間件就是一些用於定製對特定請求的處理過程的函數。做爲中間件的函數是相互獨立的,能夠提供諸如記錄日誌、返回特定響應報頭、壓縮等操做。javascript

一樣的,在 Redux 中,action 對象對應於 Express 中的客戶端請求,會被 Store 中的中間件依次處理。以下圖所示:java

image

中間件能夠實現通用邏輯的重用,經過組合不一樣中間件能夠完成複雜功能。它具備下面特色:git

  • 中間件是獨立的函數
  • 中間件能夠組合使用
  • 中間件有一個統一的接口

這裏採用了 AOP (面向切面編程)的思想。github

對於面向對象思想,當須要對邏輯增長擴展功能時(如發送請求前的校驗、打印日誌等),咱們只能在所在功能模塊添加額外的擴展功能;或者選擇共有類經過繼承方式調用,可是這將致使共有類的膨脹。不然,就只有將其散落在業務邏輯的各個角落,形成代碼耦合。typescript

而使用 AOP 的思想,能夠解決代碼冗餘、耦合問題。咱們能夠將擴展功能代碼單獨放入一個切面,待執行的時候纔將其載入到須要擴展功能的位置,即切點。這樣的好處是不用更改自己的業務邏輯代碼,這種經過串聯的方式傳遞調用擴展功能也是中間件的原理。編程

從零開發一箇中間件

中間件須要有統一的接口,才能實現自由組合。每一箇中間件必須被定義成一個函數 f1,返回一個接收 next 參數的函數 f2,而 f2 又返回一個接收 action 參數的函數 f3next 參數自己也是一個函數,中間件調用 next 函數通知 Redux 處理工做已經結束,能夠將 action 對象傳遞給下一個中間件或 Reducer。redux

最簡單的中間件

例如,能夠編寫一個什麼事都不作的中間件:數組

function doNothingMiddleware ({ dispatch, getState }) {
  return function (next) {
    return function (action) {
      return next(action)
    }
  }
}

用箭頭函數進行簡化:閉包

const doNothingMiddleware = ({ dispatch, getState }) => (next) => (action) => next(action)

能夠看出,中間件經過定義函數、接收函數、返回函數,來對 action 對象進行處理。app

不論是中間件的實現,仍是中間件的使用,都是讓每一個函數的功能儘可能小,而後經過函數的嵌套組合來實現複雜功能,這是函數式編程中的重要思想。

logger

接下來作一些擴展,實現一個能夠記錄日誌的中間件:

const logger = ({ dispatch, getState }) => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', getState())
  return result
}

通過這個中間件的處理,每次觸發 action 時,會首先打印出當前 action 的信息,而後調用 next(action),將 action 對象傳遞給下一個中間件或 Reducer,返回處理後的結果,而後獲取最新 state 並打印。

crashReporter

一個記錄錯誤日誌的中間件:

const crashReporter = ({ dispatch, getState }) => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    throw err
  }
}

try ... catch ...next(action) 包裹,對於正常 action 不作任何處理,對於出錯的 action 處理將會捕獲異常,打印錯誤日誌,並將異常拋出。

在 Redux 中組合使用中間件

在 Redux 中,經過 applyMiddleware 來使用中間件:

import { createStore, combineReducers, applyMiddleware } from 'redux'

const todoApp = combineReducers(reducers)
const store = createStore(
  todoApp,
  applyMiddleware(logger, crashReporter)
)

經過 store.dispatch(addTodo('use redux')) 觸發一個 action,將前後被 loggercrashReporter 兩個中間件處理。

接下來咱們看看 Redux 是怎麼實現中間件調用的,這是 Redux 中的部分源碼:

export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  return (createStore: StoreCreator) => <S, A extends AnyAction>(
    reducer: Reducer<S, A>,
    ...args: any[]
  ) => {
    const store = createStore(reducer, ...args)
    let dispatch: Dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

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

    return {
      ...store,
      dispatch
    }
  }
}

假設有三個中間件 M1,M2,M3,應用 applyMiddleware(M1, M2, M3) 將返回一個閉包函數,該函數接收 createStore 函數做爲參數,使得建立狀態樹 store 的步驟在這個閉包內執行;而後將 store 從新組裝成 middlewareAPI 做爲新的 store,即中間件最外層函數的參數,這樣中間件就能夠根據狀態樹進行各類操做了。

對中間件處理的關鍵邏輯在於

const chain = middlewares.map(middleware => middleware(middlewareAPI))
  dispatch = compose(...chain)(store.dispatch)

首先,將 applyMiddleware 函數中傳入的中間件按順序生成一個隊列 chain,隊列中每一個元素都是中間件調用後的結果,它們都具備相同的結構 next => action => {}

而後,經過 compose 方法,將這些中間件隊列串聯起來。compose 是一個從右向左的嵌套包裹函數,也是函數式編程中的經常使用範式,實現以下:

export default function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return <T>(arg: T) => arg
  }

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

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

假設 chain 是包含 C一、C二、C3(對應 M1,M2,M3 第一層函數返回值) 三個函數的數組,那麼 compose(...chain)(store.dispatch) 即爲 C1(C2(C3(store.dispatch))),並且:

  • applyMiddleware 的最後一箇中間件 M3 中的 next 就是原始的 store.dispatch
  • M2 中的 nextC3(store.dispatch)
  • M1 中的 nextC2(C3(store.dispatch))

最終將 C1(C2(C3(store.dispatch))) 做爲新的 dispatch 掛載在 store 中返回給用戶,做爲用戶實際調用的 dispatch 方法。因爲已經層層調用了 C3,C2,C1,中間件的結構已經從 next => action => {} 被拆解爲 acion => {}

咱們能夠梳理一遍當用戶觸發一個 action 的完整流程:

  1. 手動觸發一個 action:store.dispatch(action)
  2. 等價於調用 C1(C2(C3(store.dispatch)))(action)
  3. 執行 C1 中的代碼,直到遇到 next(action),此時的 next 爲 M1 中的 next,即:C2(C3(store.dispatch))
  4. 執行 C2(C3(store.dispatch))(action),直到遇到 next(action),此時的 next 爲 M2 中的 next,即:C3(store.dispatch)
  5. 執行 C3(store.dispatch)(action),直到遇到 next(action),此時的 next 爲 M3 中的 next,即:store.dispatch
  6. 執行 store.dispatch(action)store.dispatch(action) 內部調用 root reducer 更新當前 state
  7. 執行 C3 中 next(action) 以後的代碼
  8. 執行 C2 中 next(action) 以後的代碼
  9. 執行 C1 中 next(action) 以後的代碼

其實這就是所謂的洋蔥模型,Koa 中的中間件執行機制也是如此。

對於上面的 applyMiddleware(logger, crashReporter),若是咱們執行

export const store = createStore(
  counter,
  applyMiddleware(logger, crashReporter)
);

store.subscribe(() => console.log("store change", store.getState()));

store.dispatch({ type: "INCREMENT" });

,結果將是

image

先觸發 logger,輸出 dispatching,執行 next(action);而後在 crashReporter 中無異常,沒有輸出;執行 Reducer,獲得新的 state,store 中監聽到狀態變化,輸出 store change;最後執行 logger 中 next 以後的語句

若是是 store.dispatch(),由於 action 必須是一個對象,因此在 crashReporter 中將會捕獲異常,並拋出錯誤,結果爲:

image

demo

redux-thunk

這個應該是最經常使用到的 Redux 中間件了,是咱們在 Redux 中處理異步請求的經常使用方案。redux-thunk 的實現很是簡單,只有14 行代碼(包括空行)

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

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

其主要邏輯爲,檢查 action 的類型,若是是函數,就執行 action 函數,並把 dispatchgetState 做爲參數傳遞進去;不然就調用 next 讓下一個中間件繼續處理 action

因此,咱們能夠經過使用 redux-thunk 來在 action 生成器(action creator)中返回一個函數而不是簡單的 action 對象。從而實現 action 的異步 dispatch,如:

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER,
  };
}

function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 1000);
  };
}

或在特定條件下才發送 action,如:

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

參考文章

《深刻淺出 React 和 Redux》·程墨

《redux 中間件入門到編寫,到改進,到出門》

相關文章
相關標籤/搜索