當一個應用足夠大的時候,咱們使用一個reducer
函數來維護state
是會碰到麻煩的,太過龐大,分支會不少,想一想都會恐怖。基於以上這一點,redux
支持拆分reducer
,每一個獨立的reducer
管理state
樹的某一塊。html
隨着應用變得愈來愈複雜,能夠考慮將 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
檢查的操做就是在返回以前,看看代碼。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
}
複製代碼
Object.keys
拿到入參對象的key
,而後聲明一個finalReducers
變量用來存方最終的reducer
。reducerKeys
,檢查每一個reducer
的正確性,好比控制的判斷,是否爲函數的判斷,若是符合規範就放到finalReducerKeys
對象中。Object.keys
獲取清洗後的key
assertReducerShape(finalReducers)
函數去檢查每一個reducer
的預期返回值,它應該符合如下:
通過了檢查,最終返回了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
複製代碼
isChanged
來表示,通過reducer
處理以後,state
是否變動了。finalReducerKeys
。reducer
和對應的key
而且根據key
獲取到state
相關的子樹。reducer(previousStateForKey, action)
獲取對應的返回值。undefined
,而後進行相應的報錯。key
中。===
進行比較新獲取的值和state
裏面的舊值,能夠看到這裏只是比較了引用,注意redcuer
裏面約束有修改都是返回一個新的state
,全部若是你直接修改舊state
引用的話,這裏的hasChanged
就會被判斷爲false
,在下一步中,若是爲false
就會返回舊的state
,數據就不會變化了。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
的結構。
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
}
}
複製代碼
createStore
傳入reducer, preloadedState
這兩個參數,也就是...args
,生成store
。dispatch
爲一個只會拋錯誤的空函數。middlewareAPI
變量,對象裏面有兩個屬性,分別爲getState
和dispatch
,這裏的dispatch
是一個函數,執行的時候會調用當前做用域的dispatch
變量,能夠看到,在這一步dispatch
仍是那個空函數。middlewares
,將構建的middlewareAPI
變量傳入,生成一個新的隊列,裏面裝的都是各個中間件執行後的返回值(通常爲函數)。compose
去生成新的dispatch
函數。store
的全部屬性返回,而後使用新生成的dispatch
去替換默認的dispatch
函數。中間件的重點就是將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)))
}
複製代碼
reduce
方法,對這些參數(函數),進行一種整合。看看官方註釋:
For example, compose(f, g, h) is identical to doing (...args) => f(g(h(...args))). 這就是爲何像
logger
這樣的中間件須要注意順序的緣由了,若是放在最後一個參數。最後一箇中間件能夠拿到最終的store.dispatch
,全部能在它的先後記錄變動,不受其餘影響。nodejs
的koa
框架的洋蔥模型與之相似。
再回到applyMiddleware
函數,通過compose
函數處理後,最後返回了一個函數。
compose(...chain)(store.dispatch)
複製代碼
再把store.dispatch
傳入到這些整合後的中間件後,獲得最後的dispatch
函數。
看了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
函數的返回值,這就是中間件的一個實現了。
store
,也就是applyMiddleware
函數在執行 const chain = middlewares.map(middleware => middleware(middlewareAPI))
會傳入的。compose(...chain)(store.dispatch)
函數獲得的,這裏會將其餘的中間件做爲參數next
傳入。能夠看到,當傳入的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
也是同理。
幾行代碼能夠作不少事情,好比中間件的串聯實現,函數式的編程使人眼花繚亂。
分析了combineReducer
和applyMiddleware
,redux
也就梳理完了。中間件的編程思想很值得借鑑,在中間件上下相互不知的狀況下,也能很好的協做。
參考文章