前情提要
认识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
感谢你的阅读~