逐行閱讀redux原始碼(二)combineReducers

santree發表於2018-11-14

前情提要

認識reducers

在我們開始學習原始碼之前,我們不妨先來看看何謂reducers:

image

如圖所見,我們可以明白, reducer 是用來對初始的狀態樹進行一些處理從而獲得一個新的狀態樹的,我們可以繼續從其使用方法看看 reducer 到底如何做到這一點:

function reducerDemo(state = {
}, action) {
switch (action.type) {
case 'isTest': return {
isTest: true
};
default: return state;

}
}複製程式碼

從我們的 reducerDemo 中,我們可以看到 reducer 接受了兩個引數:

  • state
  • action

通過對 action 中的 type 的判斷,我們可以用來確定當前 reducer 是對指定 typeaction 進行響應,從而對初始的 state 進行一些修改,獲得修改之後的 state 的。從之前我們在 createStore 中看到的情況:

currentState = currentReducer(currentState, action)複製程式碼

每次 reducer 都會使用上一次的 state,然後處理之後獲得新的 state

但是光是如此的話,在處理大型專案的時候我們似乎有點捉襟見肘,因為一個store只能接受一個reducer,在大型專案中我們通常會有非常非常多的 action 用來對狀態樹進行修改,當然你也可以在 reducer 中宣告海量的 switch...case.. 來實現對單個action的響應修改,但是當你這樣做的時候,你會發現你的reducer越來越大,處理過程越來越複雜,各個業務邏輯之間的耦合度越來越高,最後你就會發現這個 reducer 將完全無法維護。

所以為了解決在大型專案中的這類問題,我們會使用多個reducer,每個reducer會去維護自己所屬的單獨業務,但是正如我們之前所說,每個store只會接受一個 reducer,那我們是如何將reducer1、reducer2、reducer3、reducer4整合成一個reducer並且返回我們所需的狀態樹的呢?

combineReducers

當然我們能想到的問題,redux 肯定也能想到,所以他們提供了 combineReducersapi讓我們可以將多個 reducer 合併成一個 reducer ,並根據對應的一些規則生成完整的狀態樹,so,讓我們進入正題,開始閱讀我們 combineReducers 的原始碼吧:

依賴

首先是combineReducers的依賴,我們能在程式碼的頭部找到它:

import ActionTypes from './utils/actionTypes'import warning from './utils/warning'import isPlainObject from './utils/isPlainObject'複製程式碼

可以看到,combineReducers僅僅依賴了之前我們在上一篇文章中提到的工具類:

  • ActionTypes(內建的actionType)
  • warning(顯式列印錯誤)
  • isPlainObject(檢測是否為物件)

錯誤資訊處理

進入正文,在combineReducers的開始部分,我們能夠發現許多用於返回錯誤資訊的方法:

  • getUndefinedStateErrorMessage(當reducer返回一個undefined值時返回的錯誤資訊)
function getUndefinedStateErrorMessage(key, action) { 
const actionType = action &
&
action.type const actionDescription = (actionType &
&
`action "${String(actionType)
}
"
`) || 'an action' return ( `Given ${actionDescription
}
, reducer "${key
}
"
returned undefined. ` + `To ignore an action, you must explicitly return the previous state. ` + `If you want this reducer to hold no value, you can return null instead of undefined.` )
}複製程式碼

從方法可知,這個處理過程中,我們傳入了key(reducer的方法名)以及action物件,之後根據action中是否存在type獲得了action的描述,最終返回了一段關於出現返回undefined值的reduceraction的描述語以及提示。

  • getUnexpectedStateShapeWarningMessage(獲取當前state中存在的沒有reducer處理的狀態的提示資訊)
function getUnexpectedStateShapeWarningMessage(  inputState,  reducers,  action,  unexpectedKeyCache) { 
const reducerKeys = Object.keys(reducers) const argumentName = action &
&
action.type === ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer' if (reducerKeys.length === 0) {
return ( 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.' )
} if (!isPlainObject(inputState)) {
return ( `The ${argumentName
}
has unexpected type of "` + {
}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + `"
. Expected argument to be an object with the following ` + `keys: "${reducerKeys.join('", "')
}
"
` )
} const unexpectedKeys = Object.keys(inputState).filter( key =>
!reducers.hasOwnProperty(key) &
&
!unexpectedKeyCache[key] ) unexpectedKeys.forEach(key =>
{
unexpectedKeyCache[key] = true
}) if (action &
&
action.type === ActionTypes.REPLACE) return if (unexpectedKeys.length >
0) {
return ( `Unexpected ${unexpectedKeys.length >
1 ? 'keys' : 'key'
}
` + `"${unexpectedKeys.join('", "')
}
"
found in ${argumentName
}
. ` + `Expected to find one of the known reducer keys instead: ` + `"${reducerKeys.join('", "')
}
"
. Unexpected keys will be ignored.` )
}
}複製程式碼

在說這段原始碼之前,我們需要稍微瞭解一下,當我們使用combineReucers,我們傳入的reducer的資料結構:

function reducer1(state={
}, action) {
switch (action.type) {
case 'xxx': return true;
default: return state;

}
}function reducer2() {...
}function reducer3() {...
}function reducer4() {...
}const rootReducer = combineReucers({
reducer1, reducer2, reducer3, reducer4
})複製程式碼

我們傳入的時以reducer的方法名作為鍵,以其函式作為值的物件,而使用rootReducer生成的store會是同樣以每個reducer的方法名作為鍵,其reducer處理之後返回的state作為值的物件,比如:

// 生成的state{ 
reducer1: state1, reducer2: state2, reducer3: state3, reducer4: state4
}複製程式碼

至於為何會這樣,我們後面再提,現在先讓我們繼續往下閱讀這個生成錯誤資訊的方法。

在這個方法中,其工作流程大概如下:

  • 宣告reducerKeys獲取當前合併的reducer的所有鍵值
  • 宣告argumentName獲取當前是否為第一次初始化store的描述
  • 當不存在reducer的時候返回拋錯資訊
  • 當傳入的state不是一個物件時,返回報錯資訊。
  • 獲取state上未被reducer處理的狀態的鍵值unexpectedKeys,並將其存入cache值中。
  • 檢測是否為內建的replace action,因為當使用storereplaceReducer時會自動觸發該內建action,並將reducer替換成傳入的,此時檢測的reducer和原狀態樹必然會存在衝突,所以在這種情況下檢測到的unexpectedKeys並不具備參考價值,將不會針對性的返回拋錯資訊,反之則會返回。

通過如上流程,我們將能對未被reducer處理的狀態進行提示。

  • assertReducerShape(檢測reducer是否符合使用規則)
function assertReducerShape(reducers) { 
Object.keys(reducers).forEach(key =>
{
const reducer = reducers[key] const initialState = reducer(undefined, {
type: ActionTypes.INIT
}) if (typeof initialState === 'undefined') {
throw new Error( `Reducer "${key
}
"
returned undefined during initialization. ` + `If the state passed to the reducer is undefined, you must ` + `explicitly return the initial state. The initial state may ` + `not be undefined. If you don't want to set a value for this reducer, ` + `you can use null instead of undefined.` )
} if ( typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === '
undefined' ) {
throw new Error( `Reducer "${key
}" returned undefined when probed with a random type. ` + `Don'
t try to handle ${
ActionTypes.INIT
} or other actions in "redux/*" ` + `namespace. They are considered private. Instead, you must return the ` + `current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined, but can be null.` )
}
})
}複製程式碼

相對之前的多次判斷,這個就要簡單暴力的多了,直接遍歷所有的reducer,首先通過傳入undefined的初始值和內建的init action,如果不能返回正確的值(返回了undefined值),那麼說明reducer並沒有針對預設屬性返回正確的值,我們將提供指定的報錯資訊。

這之後又使用reducer處理了undefined初始值和內建隨機action的情況,這一步的目的是為了排除使用者為了避免第一步的判斷,從而手動針對內建init action進行處理,如果使用者確實做了這種處理,就丟擲對應錯誤資訊。

如此,我們對combineReucers的錯誤資訊處理已經有了大概的瞭解,其大致功能如下:

  • 判斷reducer是否是合規的
  • 找出哪些reducer不合規
  • 判斷狀態樹上有哪些沒有被reducer處理的狀態

瞭解了這些之後,我們便可以進入真正的combineReducers了。

合併reducers

export default function combineReducers(reducers) { 
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) let unexpectedKeyCache if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {
}
} let shapeAssertionError try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
} return function combination(state = {
}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
} if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) {
warning(warningMessage)
}
} 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
} return hasChanged ? nextState : state
}
}複製程式碼

首先我們看到變數宣告部分:

  • reducerKeys (reducer在物件中的方法名)
  • finalReducers (最終合併生成的的reducers)

接下來,該方法迴圈遍歷了reducerKeys,並在產品級(production)環境下對型別為undefinedreducer進行了過濾和列印警告處理,其後又將符合規範的reducer放到了finalReducer中,這一步是為了儘量減少後面的流程受到空值reducer的影響。

然後combineReducers進一步的對這些非空reducer進行了處理,檢測其中是否還有不合規範的reducer(通過assertReducerShape),並通過try catch將這個錯誤儲存到shapeAssertionError變數中。

正如我們一直所說,reducer需要是一個function,所以我們的combineReducer將是一個高階函式,其會返回一個新的reducer,也就是原始碼中的combination

在返回的combination中,會檢測是否有shapeAssertionError,如果有呼叫該reducer時將終止當前流程,丟擲一個錯誤,並且在產品級環境下,還會檢測是否有未被reducer處理的state並列印出來進行提示(不中斷流程)。

最後才是整個combination的核心部分,首先其宣告瞭一個變數來標識當前狀態樹是否更改,並宣告瞭一個空的物件用來存放接下來會發生改變的狀態,然後其遍歷了整個finalReducer,通過每個reducer處理當前state,並將其獲得的每個值和之前狀態樹中的對應key值得狀態值進行對比,如果不一致,那麼就更新hasChanged狀態,並將新的狀態值放到指定key值得state中,且更新整個狀態樹,當然其中還是會對出現異常state返回值的異常處理。

結語

到此,我們已經通讀了combineReducers中的所有程式碼,也讓我們稍微對使用combineReducer時需要注意的幾個點做一個總結:

  • 每個reducer必須要有非undefined的返回值
  • 不要使用reducer手動去操作內建的action
  • combineReducers需要注意傳入的物件每個鍵必須對應一個型別為functionreducer(廢話

請大家記住這幾個點,在這些前提下能夠幫助你更快的理解我們的combineReducers

感謝你的閱讀~

來源:https://juejin.im/post/5bebf227518825170d1aa30b

相關文章