Redux Middleware 原理淺析

Redux 是一個基於Flux架構的 JavaScript 應用狀態管理庫,提供可預測性的狀態管理方案。其中,middleware更是Redux中一個重要的概念,它存在使得Redux應用更加靈活、可擴展、可維護。本文中,我們將探討 Redux middleware的運行機制和實現原理,最後帶您輕鬆實現一個自己的middleware。無論你是初學者還是有一定經驗的開發者,相信本文都能給你帶來一些新的啓示和技巧。讓我們一起探索Redux middleware的魅力吧!

什麼是 Middleware

Redux middleware是一種可插拔的機制,用於在 Reduxdispatch函數被調用後, redcer 處理 action 之前,對 action 進行攔截、變換、增強等操作。Redux middleware可以用於很多場景,例如:

middleware 簡化後的核心邏輯如下:

const middleware = store =next =action ={
  // do something before dispatching the action
  const result = next(action);
  // do something after dispatching the action
  return result;
};

通過以上代碼可以看出middleware本質上就是一個接受storenextaction三個參數的函數。其中,storeReduxstore對象,nextdispatch函數,action是當前的action對象。

使用 Middleware

Redux中使用middleware非常簡單,只需要在創建 store 的時候使用applyMiddleware函數將middleware應用到 store 上即可,

例如:

import { createStore, applyMiddleware } from 'redux'import rootReducer from './reducers'
import middleware1 from './middleware/middleware1'
import middleware2 from './middleware/middleware2'
const store = createStore(
  rootReducer,
  applyMiddleware(middleware1, middleware2)
)

在上面的代碼中,我們使用了applyMiddleware函數將middleware1middleware2應用到 store 上。這樣,當我們調用 store.dispatch(action) 時,middleware 就會被依次執行,直到 reducer 處理 action。

Middleware 內部運行機制及原理剖析

我們通過上文的使用方式發現,middleware是通過createStore來增強和擴展原來的dispatch。下面我們就從createStore入手,逐步對middleware進行剖析:

createStore 源碼分析

//簡化後的源碼
import { Action } from './types/actions'
import { Reducer } from './types/reducers'
export function createStore<
  S,
  A extends Action,
  Ext extends {} = {},
  StateExt extends {} = {}
>(
  reducer: Reducer<S, A>,
  preloadedState?: PreloadedState<S> | StoreEnhancer<Ext, StateExt>,
  enhancer?: StoreEnhancer<Ext, StateExt>
): Store<S, A, StateExt> & Ext {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        `Expected the enhancer to be a function. Instead, received: '${kindOf(enhancer)}'`
      )
    }
    return enhancer(createStore)(
      reducer,
      preloadedState as PreloadedState<S>
    ) as Store<S, A, StateExt> & Ext
  }
  let currentReducer = reducer
  let currentState = preloadedState as S
  let currentListeners: Map<number, ListenerCallback> | null = new Map()
  let nextListeners = currentListeners
  let listenerIdCounter = 0
  let isDispatching = false
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = new Map()
      currentListeners.forEach((listener, key) ={
        nextListeners.set(key, listener)
      })
    }
  }
  function getState(): S {
        ...
  }
  function subscribe(listener: () => void) {
         ...
    let isSubscribed = true
    ensureCanMutateNextListeners()
    const listenerId = listenerIdCounter++
    nextListeners.set(listenerId, listener)
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }
      if (isDispatching) {
        throw new Error('...')
      }
      isSubscribed = false
      ensureCanMutateNextListeners()
      nextListeners.delete(listenerId)
      currentListeners = null
    }
  }
  function dispatch(action: A) {
        ...
  }
  dispatch({ type: ActionTypes.INIT } as A)
  const store = {
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState
  } as unknown as Store<S, A, StateExt> & Ext
  return store
}

從以上代碼,createStore方法接收三個參數:reducerpreloadedStateenhancer。如果傳入了enhancer則使用 enhancer 來增強 store(實際上是通過重寫 createStore 來增強 dispatch),否則就返回一個包含getState、dispatchsubscribe方法的 store 對象。其中,這裏的第三個參數 enhancer 就是我們下文要分析的 applyMiddleWare

applyMiddleware 源碼分析

//簡化後的源碼
export default function applyMiddleware(
  ...middlewares: Middleware[]
): StoreEnhancer<any> {
  return createStore =>
    <S, A extends AnyAction>(
      reducer: Reducer<S, A>,
      preloadedState?: PreloadedState<S>
    ) ={
      const store = createStore(reducer, preloadedState)
      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
      }
    }
}

如上所示,先通過輪詢執行middleware柯里化函數第一層來爲每個 middleware 函數提供getStatedispatch;再通過compose將所有middleware串聯起來形成一個函數鏈,從而實現對 Redux 數據的攔截和處理,並最終返回一個增強版的 dispatch。我們看到在applyMiddleWarecompose是核心邏輯,下面我們具體分析下 compose 是如何進行 middleware 函數聚合的。

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) => a(b(...args)));
}

如上所示,這段代碼首先判斷 funcs 數組的長度,如果長度爲 0,則直接返回一個函數;如果長度爲 1,則直接返回 funcs[0];如果長度大於 1,則使用reduce方法通過把後一個的middleware的結果當成參數傳遞給下一個middleware的方式將 funcs 數組中的函數依次組合起來。這裏的 func 也就是接收nextdispatch作爲參數的middleware柯里化函數第二層,func 執行後會返回一個新函數action => next(action)。最終compose返回一個新函數,並按照從右到左的順序依次調用每個 func 進行處理,這個函數就是增強版的dispatch

接下來,我們可以用 “把大象放冰箱” 這個哲理題作爲一個示例,來繼續加深對compose函數的理解:

function putElephantInFridge(){
  console.log('打開冰箱門');
  console.log('把大象放進去');
  console.log('關上冰箱門');
}

這個函數實現起來雖然簡單,但不好進行繼續擴展。爲了便於擴展我們把這個大函數拆解並抽象化,讓每個函數都是獨立的,只負責完成自己的任務,最後再實現一個通用函數來獲取最後的結果:

function openFridgeDoor() {
  console.log('打開冰箱門');
}
function putSomethingInFridge(something) {
  console.log(`${something}放進去`);
}
function closeFridgeDoor() {
  console.log('關上冰箱門');
}
const putInFridge = (something)=>compose(closeFridgeDoor,()=>{putSomethingInFridge(something)},openFridgeDoor)();
const putInFridgeAndNotClose = (something)=>compose(()=>{putSomethingInFridge(something)},openFridgeDoor)();
putInFridge('牛奶'); // 打開冰箱門  把牛奶放進去  關上冰箱門
putInFridgeAndNotClose('蘋果'); // 打開冰箱門  把蘋果放進去

在上面的代碼中,我們使用compose函數將三個單獨的函數組合成了一個函數putInFridge,該函數接收一個參數 something,並依次執行三個步驟,最終將 something 放進了冰箱中。另外,我們也可以將其中兩個函數組合成函數putInFridgeAndNotClose。由上我們看到,compose 函數是非常實用的一個函數,通過它可以將任意多個函數組合在一起,實現更加靈活和有序的函數調用,增強了程序的複用性、可讀性、可測性。

Middleware 實現方式

在 Redux 應用中,我們可以使用多種方式來實現 middleware。下面我們將介紹兩種主要的實現方法:

基於洋蔥模型實現

用過expresskoa同學應該都知道它們也都有 middleware 概念,Redux middleware的實現和 koa 的洋蔥模型的機制相似。Redux middlewaredispatch action和到達reducer之間提供第三方擴展點,這種實現方式的代碼結構類似於洋蔥,形成了一層層的包裹,每一層都可以執行一些操作,在每一層中可以對 action 進行處理。

基於裝飾器實現

基於裝飾器相對於基於洋蔥模型更加直觀和易於理解,但是它需要使用 ES7 中的裝飾器語法,需要做一定的兼容性處理,這裏不做過多闡述。

編寫自定義 Middleware

基於以上簡要剖析,我們接下來可以進行開發屬於自己的middleware。下文是一個最簡單的middleware

const loggerMiddleware = storeAPI =next =action ={
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', storeAPI.getState())
  return result
}

這個簡易版的 logger 負責在控制檯中打印出當前動作的類型及當狀態發生變化時打印出最新的狀態。使用它可以幫助開發人員更快地發現應用中的異常。

爲什麼要使用 storeAPI => next => action => 這種形式呢?

要回答這個問題我們可以先來看下 Redux 三大原則:

Redux middleware 的 storeAPI 參數包含了整個 Redux store 的狀態和dispatch方法,這保證了 Redux 應用中只有一個單一的數據源;middleware中的狀態是隻讀的,不能被直接修改狀態;Redux middleware 中的 next 函數它接收一個動作作爲參數,並返回一個新的函數。因此,採用這種形式正是更好的遵循 Redux 的設計原則,確保 Redux 應用程序的可預測性、可維護性和可擴展性。另外,在 Redux 社區中也有對使用這種形式的不同聲音,他們認爲 “there is essentially no need to split the arguments into different function calls”、“minor change could promote authors' familiarity and understanding, thus encourage the development of additional middleware to Redux”,關於這塊您可以自行擴展閱讀。

總結

middleware 是 Redux 應用中的一個重要概念,Redux middleware 的原理是基於 Redux 原則和函數式編程思想,通過函數柯里化和函數組合來實現對 dispatch 的增強,使得在數據流傳遞過程中可以插入一些自定義的操作。最後,希望本文能夠幫助讀者加深對 middleware 原理的理解,助您開發出更加穩定、高效的 react 應用。

參考


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JDM9GOYvkHui7lpwINxL8g