解讀 Redux 中介軟體的原理

表示很不蛋定發表於2017-10-10

Redux 的中介軟體提供的是位於 action 被髮起之後,到達 reducer 之前的擴充套件點,換而言之,原本 view -> action -> reducer -> store 的資料流加上中介軟體後變成了 view -> action -> middleware -> reducer -> store ,在這一環節我們可以做一些 “副作用” 的操作,如 非同步請求、列印日誌等。

使用示例

以日誌輸出 Logger 為例:

import { createStore, applyMiddleware } from 'redux'
/** 定義初始 state**/
const initState = {
  score : 0.5
}
/** 定義 reducer**/
const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_SCORE':
      return { ...state, score:action.score }
    default:
      break
  }
}

/** 定義中介軟體 **/
const logger = ({ getState, dispatch }) => next => action => {
  console.log('【logger】即將執行:', action)

    // 呼叫 middleware 鏈中下一個 middleware 的 dispatch。
  let returnValue = next(action)

  console.log('【logger】執行完成後 state:', getState())
  return returnValue
}

/** 建立 store**/
let store = createStore(reducer, initState, applyMiddleware(logger))

/** 現在嘗試傳送一個 action**/
store.dispatch({
  type: 'CHANGE_SCORE',
  score: 0.8
})
/** 列印:**/
// 【logger】即將執行: { type: 'CHANGE_SCORE', score: 0.8 }
// 【logger】執行完成後 state: { score: 0.8 }複製程式碼

解讀

要理解上面這段程式碼,首先要從建立storecreateStore函式說起:
createStore函式接收引數為(reducer, [preloadedState], enhancer),其中preloadedState為初始state,那麼 enhancer 又是什麼呢?從官方文件可以看到,StoreCreator 的函式簽名為

type StoreCreator = (reducer: Reducer, initialState: ?State) => Store複製程式碼

是一個普通的建立 store 的函式,而 enhancer 的簽名為

type enhancer = (next: StoreCreator) => StoreCreator複製程式碼

可知enhancer是一個組合 StoreCreator高階函式, 返回的是一個新的強化過的 StoreCreator,再執行StoreCreator就能得到一個加強版的 store。
在本例裡形參enhancer即為applyMiddleware,從下面的原始碼可知,applyMiddleware 改寫了 storedispatch 方法,新的 dispatch 即是被所傳入的中介軟體包裝過的。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    // 接收 createStore 引數
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    // 傳遞給中介軟體的引數
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }

    // 註冊中介軟體呼叫鏈,並由此可知,所有的中介軟體最外層函式接收的引數都是{getState,dispatch}
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    //compose 函式起到程式碼組合的作用:compose(f, g, h)(...args) 效果等同於 f(g(h(...args))),具體實現可參見附錄。從此也可見:所有的中介軟體最二層函式接收的引數為 dispatch,一般我們在定義中介軟體時這個形參不叫 dispatch 而叫 next,是由於此時的 dispatch 不一定是原始 store.dispatch,有可能是被包裝過的新的 dispatch。
    dispatch = compose(...chain)(store.dispatch)

    // 返回經 middlewares 增強後的 createStore
    return {
      ...store,
      dispatch
    }
  }
}複製程式碼

這樣下來,原來執行 dispatch(action) 的地方變成了執行新函式

(action)=>{
    console.log('【logger】即將執行:', action)
    dispatch(action)
    console.log('【logger】執行完成後 state:', getState())
}複製程式碼

這樣就實現了action -> reducer的攔截,所以每次觸發 action 都能被 log 出來了,?。

對於非同步中介軟體的情況也同理 , 以 redux-thunk 為例:

// 這是簡化後的 redux-thunk
const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }
    return next(action);
  };複製程式碼

這裡可以看到,當 dispatch 的收到的 action 為函式時,將試圖巢狀執行這個函式。套用這個中介軟體後的 dispatch 方法就更 “聰明” 了,這就是為什麼 redux 中規定 action 必須為純物件而在 redux-thunk 中傳的 action 卻是 function 而不會報錯的原因。

小結

redux 中介軟體通過改寫 store.dispatch 方法實現了action -> reducer的攔截,從上面的描述中可以更加清晰地理解 redux 中介軟體的洋蔥圈模型

中介軟體A -> 中介軟體B-> 中介軟體C-> 原始 dispatch -> 中介軟體C -> 中介軟體B -> 中介軟體A複製程式碼

這也就提醒我們使用中介軟體時需要注意這個中介軟體是在什麼時候 “搞事情” 的,比如 redux-thunk 在執行 next(action) 前就攔截了型別為 functionaction,而 redux-saga 就在 next(action) 才會觸發監聽 sagaEmitter.emit(action), 並不會攔截已有 action 到達 reducer。

附: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))
}複製程式碼

精妙之處就在巧妙的利用了 Array.prototype.reduceRight(callback[, initialValue]) 這個我們平時不怎麼用到的函式。該方法將陣列中每一項從右向左呼叫callback,本例中的callback即為

(composed, f) => f(composed)複製程式碼

initialValue初始值是陣列中最後一個func。

這裡下面是另一種實現:

const compose = (...funcs) => (result) => {
    //... 省略邊界判斷
    for (var i = funcs.length - 1; i > -1; i--) {
      result = funcs[i].call(this, result);
    }
    return result;
}複製程式碼

這種寫法就更容易理解為什麼compose(f, g, h)(...args)效果等同於 f(g(h(...args))),但是就沒有上面那種優雅?。

相關文章