Redux 梳理分析【二:combineReducers和中間件】

當一個應用足夠大的時候,咱們使用一個reducer函數來維護state是會碰到麻煩的,太過龐大,分支會不少,想一想都會恐怖。基於以上這一點,redux支持拆分reducer,每一個獨立的reducer管理state樹的某一塊。html

combineReducers 函數

隨着應用變得愈來愈複雜,能夠考慮將 reducer 函數 拆分紅多個單獨的函數,拆分後的每一個函數負責獨立管理 state 的一部分。combineReducers 輔助函數的做用是,把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數,而後就能夠對這個 reducer 調用 createStore 方法。vue

根據redux文檔介紹,來看一下這個函數的實現。node

export default function combineReducers(reducers) {
...
  return function combination(state = {}, action) {
  ...
  }
}
複製代碼

先看一下函數的結構,就如文檔所說,傳入一個key-value對象,value爲拆分的各個reducer,而後返回一個reducer函數,就如代碼裏面的combination函數,看入參就知道和reducer函數一致。git

檢查傳入的 reducers 對象的合理性

檢查的操做就是在返回以前,看看代碼。github

const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
  const key = reducerKeys[i]

  if (process.env.NODE_ENV !== 'production') {
    if (typeof reducers[key] === 'undefined') {
      warning(`No reducer provided for key "${key}"`)
    }
  }

  if (typeof reducers[key] === 'function') {
    finalReducers[key] = reducers[key]
  }
}
const finalReducerKeys = Object.keys(finalReducers)

// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
  unexpectedKeyCache = {}
}

let shapeAssertionError
try {
  assertReducerShape(finalReducers)
} catch (e) {
  shapeAssertionError = e
}
複製代碼
  1. 使用Object.keys拿到入參對象的key,而後聲明一個finalReducers變量用來存方最終的reducer
  2. 遍歷reducerKeys,檢查每一個reducer的正確性,好比控制的判斷,是否爲函數的判斷,若是符合規範就放到finalReducerKeys對象中。
  3. 使用Object.keys獲取清洗後的key
  4. 經過assertReducerShape(finalReducers)函數去檢查每一個reducer的預期返回值,它應該符合如下:
    1. 全部未匹配到的 action,必須把它接收到的第一個參數也就是那個 state 原封不動返回。
    2. 永遠不能返回 undefined。當過早 return 時很是容易犯這個錯誤,爲了不錯誤擴散,遇到這種狀況時 combineReducers 會拋異常。
    3. 若是傳入的 state 就是 undefined,必定要返回對應 reducer 的初始 state。

combination 函數

通過了檢查,最終返回了reducer函數,相比咱們直接寫reducer函數,這裏面預置了一些操做,重點就是來協調各個reducer的返回值。vuex

if (shapeAssertionError) {
  throw shapeAssertionError
}

if (process.env.NODE_ENV !== 'production') {
  const warningMessage = getUnexpectedStateShapeWarningMessage(
    state,
    finalReducers,
    action,
    unexpectedKeyCache
  )
  if (warningMessage) {
    warning(warningMessage)
  }
}
複製代碼

若是以前檢查有警告或者錯誤,在執行reducer的時候就直接拋出。編程

最後在調用dispatch函數以後,處理state的代碼以下:redux

let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i]
  const reducer = finalReducers[key]
  const previousStateForKey = state[key]
  const nextStateForKey = reducer(previousStateForKey, action)
  if (typeof nextStateForKey === 'undefined') {
    const errorMessage = getUndefinedStateErrorMessage(key, action)
    throw new Error(errorMessage)
  }
  nextState[key] = nextStateForKey
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
  hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
複製代碼
  1. 聲明一個變量isChanged來表示,通過reducer處理以後,state是否變動了。
  2. 遍歷 finalReducerKeys
  3. 獲取reducer和對應的key而且根據key獲取到state相關的子樹。
  4. 執行reducer(previousStateForKey, action)獲取對應的返回值。
  5. 判斷返回值是否爲undefined,而後進行相應的報錯。
  6. 將返回值賦值到對應的key中。
  7. 使用===進行比較新獲取的值和state裏面的舊值,能夠看到這裏只是比較了引用,注意redcuer裏面約束有修改都是返回一個新的state,全部若是你直接修改舊state引用的話,這裏的hasChanged就會被判斷爲false,在下一步中,若是爲false就會返回舊的state,數據就不會變化了。
  8. 最後遍歷完以後,經過hasChanged判斷返回原始值仍是新值。

添加中件件

當咱們須要使用異步處理state的時候,因爲reducer必需要是純函數,這和redux的設計理念有關,爲了能夠能追蹤到每次state的變化,reducer的每次返回值必須是肯定的,才能追蹤到。具體放在後面講。api

當使用中間件,咱們須要經過applyMiddleware去整合中間件,而後傳入到createStore函數中,這時候相應的流程會發生變化。數組

先看看createStore函數對這部分的處理。

if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }

  return enhancer(createStore)(reducer, preloadedState)
}
複製代碼

這裏的enhancer就是applyMiddleware(thunk, logger, ...)執行後的返回值。能夠看到,enhancer函數執行,須要把createStore函數傳入,說明enhancer內部會本身去處理一些其餘操做後,再回來調用createStore生成store

applyMiddleware 函數

首先看一下applyMiddleware的結構。

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
  ...
  }
}
複製代碼

能夠看到applyMiddleware函數啥都沒幹,只是對傳入的middlewares參數造成了一個閉包,把這個變量緩存起來了。確實很函數式。

接下來看一下它的返回的這個函數:

createStore => (...args) => {}
複製代碼

它返回的這個函數也只是把createStore緩存(柯里化綁定)了下來,目前在createStore執行到了這一步enhancer(createStore)

再看看這個返回函數執行後的返回函數。

(...args) => {
  const store = createStore(...args)
  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: (...args) => dispatch(...args)
  }
  const chain = middlewares.map(middleware => middleware(middlewareAPI))
  dispatch = compose(...chain)(store.dispatch)

  return {
    ...store,
    dispatch
  }
}
複製代碼
  1. 調用createStore傳入reducer, preloadedState這兩個參數,也就是...args,生成store
  2. 聲明變量dispatch爲一個只會拋錯誤的空函數。
  3. 構造 middlewareAPI變量,對象裏面有兩個屬性,分別爲getStatedispatch,這裏的dispatch是一個函數,執行的時候會調用當前做用域的dispatch變量,能夠看到,在這一步dispatch仍是那個空函數。
  4. 遍歷傳入的middlewares,將構建的middlewareAPI變量傳入,生成一個新的隊列,裏面裝的都是各個中間件執行後的返回值(通常爲函數)。
  5. 經過函數 compose 去生成新的dispatch函數。
  6. 最後把store的全部屬性返回,而後使用新生成的dispatch去替換默認的dispatch函數。

compose 函數

中間件的重點就是將dispatch替換成了新生成的dispatch函數,以致於能夠在最後調用store.dispatch以前作一些其餘的操做。生成的核心在於compose函數,接下來看看。

export default 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)))
}
複製代碼
  1. 若是參數的長度爲0,就返回兜底一個函數,這函數只會把傳入的形參返回,沒有其餘操做。
  2. 若是參數的長度爲1,就將這個元素返回。
  3. 這個狀況就是說有多個參數,而後調用數組的reduce方法,對這些參數(函數),進行一種整合。看看官方註釋:

    For example, compose(f, g, h) is identical to doing (...args) => f(g(h(...args))). 這就是爲何像logger這樣的中間件須要注意順序的緣由了,若是放在最後一個參數。最後一箇中間件能夠拿到最終的store.dispatch,全部能在它的先後記錄變動,不受其餘影響。nodejskoa框架的洋蔥模型與之相似。

再回到applyMiddleware函數,通過compose函數處理後,最後返回了一個函數。

compose(...chain)(store.dispatch)
複製代碼

再把store.dispatch傳入到這些整合後的中間件後,獲得最後的dispatch函數。

redux-thunk 中間件

看了redux是怎麼處理整合中間件的,看一下redux-thunk的實現,加深一下印象。

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;
複製代碼

能夠看到最終導出的是createThunkMiddleware函數的返回值,這就是中間件的一個實現了。

  1. 第一個函數,獲得的是store,也就是applyMiddleware函數在執行 const chain = middlewares.map(middleware => middleware(middlewareAPI)) 會傳入的。
  2. 第二個函數,是在compose(...chain)(store.dispatch)函數獲得的,這裏會將其餘的中間件做爲參數next傳入。
  3. 第三個函數,就是用來實現本身的邏輯了,攔截或者進行日誌打印。

能夠看到,當傳入的action爲函數的時候,直接就return了,打斷了中間件的pie執行,而是去執行了action函數裏面的一些異步操做,最後異步成功或者失敗了,又從新調用dispatch,從新啓動中間件的pie

尾巴

上面說到,爲何reducer爲何必定須要是純函數?下面說說我的理解。

經過源碼,能夠反應出來。hasChanged = hasChanged || nextStateForKey !== previousStateForKey ... return hasChanged ? nextState : state

從這一點能夠看到,是否變化redux只是簡單的使用了精確等於來判斷的,若是reducer是直接修改舊值,那麼這裏的判斷會將修改後的丟棄掉了。那麼爲何redux要這麼設計呢?我在網上查了一些文章,說的最多的就是說,若是想要判斷A、B兩對象是否相對,就只能深度對比每個屬性,咱們知道redux應用在大型項目上,state的結構會很龐大,變動頻率也是很高的,每次都進行深度比較,消耗很大。全部redux就把這個問題給拋給開發者了。

還有爲何reducer或者vuex裏面的mutation中,不能執行異步操做,引用·vuex官方文檔:


Mutation 必須是同步函數 一條重要的原則就是要記住 mutation 必須是同步函數。爲何?請參考下面的例子:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}
複製代碼

如今想象,咱們正在 debug 一個 app 而且觀察 devtool 中的 mutation 日誌。每一條 mutation 被記錄,devtools 都須要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation 中的異步函數中的回調讓這不可能完成:由於當 mutation 觸發的時候,回調函數尚未被調用,devtools 不知道何時回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。


文檔地址reducer也是同理。

小結

幾行代碼能夠作不少事情,好比中間件的串聯實現,函數式的編程使人眼花繚亂。

分析了combineReducerapplyMiddlewareredux也就梳理完了。中間件的編程思想很值得借鑑,在中間件上下相互不知的狀況下,也能很好的協做。

參考文章

  1. 圖解Redux中middleware的洋蔥模型

原文地址

相關文章
相關標籤/搜索