理解 Redux 原始碼(四)- applyMiddleware

  在聊 applyMiddleware 前想先稍微提一下這個方法的脈絡,我當初直接看這個方法時整個是一頭霧水,在好好爬了一遍官方文件後才覺得有比較理解,而我認為先了解 enhancer 以及 middleware 會比較好瞭解這整個方法,但如果已經很熟悉的話可以直接跳到第三部分看原始碼。

  先看一下在實際使用時的寫法

import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { someMiddleware, otherMiddleware } from './middlewares'

const store = createStore(rootReducer, undefined, applyMiddleware(someMiddleware, otherMiddleware))

export default store

  看得出 applyMiddleware 帶入 middleware 並呼叫後會返回一個 enhancer 被帶入 createStore,被以 enhancer(createStore)(reducer, preloadedState) 的形式呼叫,那 enhancer 又是什麼?


一、enhancer

  在官網上對於 enhancer 的型別描述是這樣的。

type StoreEnhancer = (next: StoreCreator) => StoreCreator

  enhancer 是一個高階函數,接受了一個 createStore 方法做為參數並返回與 createStore 同樣型別的方法,目的是將原本 createStore 內的方法增加或者修改一些功能,看一下官網的兩個 enhancer 的例子

const sayHiOnDispatch = (createStore) => {
return (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers)

function newDispatch(action) {
const result = store.dispatch(action)
console.log('Hi!')
return result
}

return { ...store, dispatch: newDispatch }
}
}

const includeMeaningOfLife = (createStore) => {
return (rootReducer, preloadedState, enhancers) => {
const store = createStore(rootReducer, preloadedState, enhancers)

function newGetState() {
return {
...store.getState(),
meaningOfLife: 42,
}
}

return { ...store, getState: newGetState }
}
}

  引入時會搭配 compose 將多個 enhancer 結合為一個。

const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)

const store = createStore(rootReducer, undefined, composedEnhancer)

export default store

  sayHiOnDispatch 改寫了原本的 dispatch 方法,在呼叫 dispatch 時會 log 出hiincludeMeaningOfLife 則是改寫原本的 getState 方法,使回傳結果多一個 meaningOfLife 的屬性。

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'

console.log('State after dispatch: ', store.getState())
// log: { someState: '...' , meaningOfLife: 42}

  因此看得出來 Redux 給了一個相當彈性的入口點,透過 enhancer 在原本的 feature 上另外增添其他的功能。

  接著再來看看 middleware。


二、middleware

  如果有寫過 server 端邏輯的話對於這個名詞應該不陌生,在真正處理請求之前會先通過一個或多個 middleware 對請求做一些邏輯處理,而 Redux 的 middleware 也是類似的概念,只不過這裡的請求變成了 dispatch 方法。

  先看看 Redux 的定義。

  這裡的 compose 就是指 functional programming 中常用到的 compose 技巧,至於如何實作待會會提到,可以先理解為,middleware 接受 dispatch 方法做為參數並返回一個新的 dispatch 方法,通常被用來處理非同步作業,或是實作 logger。

  而 middleware 有一定的格式,如果想自己寫 custom middleware 的時候必須照著官方格式寫

// Middleware written as ES5 functions

// Outer function:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here

return next(action)
}
}
}

  看起來有點嚇人,但是如果拆成兩個部分就比較好理解了。

  1. exampleMiddleware

  exampleMiddlewareapplyMiddleware 呼叫時就會被呼叫並傳入 storeAPI(也就是 dispatch, getState 方法),目的是為了讓 custom middleware 內部可以取用到這兩個方法。

  1. wrapDispatch

  這個方法才是真正 middleware 的核心,方法呼叫後會返回一個handleAction方法,而仔細看會發現handleAction 和 dispatch 方法一樣都接受一個 action 做為參數,原來其實handleAction就是包裝後的 dispatch 方法!wrapDispatch接受的參數 next 是下一個 middleware 的 handleAction 方法,而如果已經沒有下一個 middleware 時,next 就是 Redux 提供的 dispatch 方法。

  為了方便理解可以先想像 applyMiddleware 只帶入一個 middleware 的場景,這時理所當然 next 就是原始的 dispatch 方法,而 wrapDispatch 也是在applyMiddleware 呼叫時就會被呼叫,這時就像 Redux 的定義說的,接受原本的 dispatch 方法做為參數,並且返回一個新的 dispatch 方法 handleAction

  雖然這裡盡可能講得不那麼抽象一點,但可能還是有點難理解,接著就看看原始碼裡面 middleware 是如何被處理的吧!


三、applyMiddleware

  有了基本對 enhancer 及 middleware 的了解後就可以來看看 applyMiddleware

import compose from './compose'

export default function applyMiddleware(...middlewares) {
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}

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

return {
...store,
dispatch
}
}
}

  可以看出 applyMiddleware 返回的方法是個非常典型的 enhancer 格式,如果先直接看最後返回的部分可以發現如同稍早說的,這個 enhancer 相當聚焦在改寫 dispatch 方法,接著來看看 enhancer 做了什麼。

  首先呼叫了 createStore 方法取得 store 物件以方便最後返回時 spread 展開取得其他方法,接著定義 middlewareAPI 讓 middleware 使用,並先用 let 定義一個可修改的 dispatch 方法,如果 dispatch 在建構時就被呼叫會馬上噴錯,拿剛才的 middleware 格式舉例示意如下

function exampleMiddleware(storeAPI) {
// storeAPI.dispatch() -> 噴錯
return function wrapDispatch(next) {
// storeAPI.dispatch() -> 噴錯
return function handleAction(action) {
// storeAPI.dispatch() -> OK!
return next(action)
}
}
}

  接著將所有傳入的 middleware 呼叫一次後放進 chain 陣列,拿剛才的例子來說此時陣列的所有 item 是每個 middleware 的 wrapDispatch 方法,然後將 chain 展開給 compose 當參數呼叫,而這裡我想先稍微說一下 compose 的作用。

  compose 在 functional programming 中是一個常見的高階函式,作用是接收多個函式作為參數並且組合成一個新的函式,只要一呼叫新的函式便會由右而左呼叫所有被傳入的函示,並將返回的結果傳入下一個函式,例如。

const f = (x) => x + 5
const g = (x) => x * 3
const h = (x) => x - 2

const composedFunc = compose(f, g, h)

  此時呼叫 composedFunc 會將參數帶入並由右而左呼叫 h, g, f

composedFunc(4) // 等於 f(g(h(4))) -> 返回 11

  大概理解以後來看一下 compose 的原始碼。

function compose(...funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return(arg) => arg
}

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

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

  有剛才的例子之後應該會比較好理解,這邊邏輯確實是有點繞,如果不夠清楚可以多看幾次,了解以後就可以回歸到 applyMiddleware 程式碼上

dispatch = compose(...chain)(store.dispatch)

  覺得亂的話先拆兩段看,然後寫白話一點

const composedWrapDispatchFunc = compose(wrapDispatch1, wrapDispatch2, wrapDispatch3...)
dispatch = composedWrapDispatchFunc(store.dispatch)

// 實際上做的事情就是...
// dispatch = wrapDispatch1(wrapDispatch2(wrapDispatch3(store.dispatch)))

  這樣就將所有 middleware 的 wrapDispatch 方法串在一起,並且產生一個新的 dispatch 方法了!

  然後恭喜有看到這邊的人,applyMiddleware 原始碼到這邊就結束了!

  如果還是覺得比較抽象的話,可以試著將最常用的 middleware 也就是 redux-thunk 的程式碼帶入看看,這邊我節錄一點 redux-thunk 程式碼並改成範例的格式。

function middleware({ dispatch, getState }) {
return function wrapDispatch(next) {
return function handleAction(action) {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
}
}
}

  套用了 redux-thunk 之後,action creator 通常會被寫成這樣的格式

function fetchPosts() {
return async function innerFunction(dispatch, getState) => {
const posts = await axios.get('/posts')
dispatch({ type: 'FETCH_POSTS_SUCCESS', payload: posts });
};
};

  在每次執行 dispatch 時會先判斷 action 型別是 function 時則直接呼叫它,而不是被傳入 dispatch 當作參數,這就也就是在此時呼叫了 innerFunction 執行非同步作業,當裡面的 dispatch 被呼叫時開始下一次的 middleware 循環。


結語

  這篇介紹了 Redux 是如何透過撰寫固定格式的 middleware、enhancer 來擴充 store 內部方法,也提到 applyMiddleware 如何透過 compose 將每個 middleware 串接在一起。

  老實說當初看到 applyMiddleware 程式碼還高興了一下想說很短應該一下就看完了,結果由於 middlewareenhancer 觀念不熟悉再回官網爬一遍之後才理解(慚愧),整個看下來反而比 combineReducers 花更久時間理解XD,於是寫這篇文章的方式就有先稍微提到一下這兩個觀念,希望能幫助到剛好有看到這篇文章的人。

  至此 Redux 中比較常用的方法就介紹完了,其實再探究下去的話還有很多可以聊的東西,像是 react-redux 如何配合 redux 去讓 react 狀態更新後重新渲染,以及為什麼 Redux 一直強調 reducer 必須是 pure function , redux-devtools-extension 是如何藉這點達成 time-traveling 等等,再追下去真的追不完啊~這些主題有機會研究一下再來和大家分享。

  另外因為小弟不常寫這樣的文章,如果有什麼不清楚或有誤之處,希望大家也不客氣鞭下去,感激不盡!



參考文獻

Redux 官方文件