前情提要
認識reducers
在我們開始學習原始碼之前,我們不妨先來看看何謂reducers:
如圖所見,我們可以明白, reducer
是用來對初始的狀態樹進行一些處理從而獲得一個新的狀態樹的,我們可以繼續從其使用方法看看 reducer
到底如何做到這一點:
function reducerDemo(state = {}, action) {
switch (action.type) {
case 'isTest':
return {
isTest: true
};
default:
return state;
}
}
複製程式碼
從我們的 reducerDemo
中,我們可以看到 reducer
接受了兩個引數:
- state
- action
通過對 action
中的 type
的判斷,我們可以用來確定當前 reducer
是對指定 type
的 action
進行響應,從而對初始的 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
肯定也能想到,所以他們提供了 combineReducers
api讓我們可以將多個 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
值的reducer
和action
的描述語以及提示。
- 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
,因為當使用store
的replaceReducer
時會自動觸發該內建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)環境下對型別為undefined
的reducer
進行了過濾和列印警告處理,其後又將符合規範的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
需要注意傳入的物件每個鍵必須對應一個型別為function
的reducer
(廢話
請大家記住這幾個點,在這些前提下能夠幫助你更快的理解我們的combineReducers
感謝你的閱讀~