前面说到,当你的state树变得非常复杂,需要存储许多状态时,你可以编写多个Reducer函数,每个Reducer函数仅处理维护一部分state,这样就可以达到模块化管理state的效果。而createStore的入参reducer是一个函数,如何将多个reducer组合成一个reducer则需要使用到Redux的另一个API:combineReducers。
多个Reducer的例子
首先应用官方文档多个reducer的例子,可能是这样:
// reducer
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
// reducer
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
通过 combineReducers 组合这两个Reducer的调用例子是这样:
const todoApp= combineReducers({todos,todoApp})
combineReducers
方法签名
首先来看看方法的签名
function combineReducers(reducers)
参数如下:
- reducers 是一个对象而不是函数数组,对象的每个字段都是一个Reducer。
方法返回
返回一个合成后的超强Reducer函数,形如:
combination(state = {}, action){
//....
}
方法作用
combineReducers 传入多个reducer,每个reducer负责处理states树的一部分,经过组合后返回一个reducer,可作为createStore的reducer参数传入。主要是为了划分state树的数据处理逻辑,当state树变得复杂时,就非常有用。
方法解读
方法的开头,定义了两个常量:
// 参数reducers所有的key值 (每个键值都是一个reducer函数的名字)
const reducerKeys = Object.keys(reducers)
// 最终的reducers对象
const finalReducers = {}
接着遍历参数 reducers 的key值,对一个个reducer进行检查:
// 遍历所有的key
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 非生产环境下 检查 reducer 是否为 undefined
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
// 检查 reducer 是否是函数
if (typeof reducers[key] === 'function') {
// 合法 加入到最终的reducer对象
finalReducers[key] = reducers[key]
}
}
这里的检查,其实就是对传入参数的过滤,即过滤掉了Reducer不为函数类型的值,最终得到一个 finalReducers 对象。当然,检查并没有那么轻松,接下来对 finalReducers 进行了更深一步的检查:
// finalReducers 的key值
const finalReducerKeys = Object.keys(finalReducers)
// 对reducer是否合法进行进一步验证
let shapeAssertionError
try {
// 将上面检查得到的 finalReducers 对象 再进行更严格的检查
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
assertReducerShape函数具体代码如下:
function assertReducerShape(reducers) {
// 遍历所有的reducer
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
// 发起一个Redux 私有的action ,同时传入的 state 是 undefined
// reducer需要返回初始state ,否则报错
const initialState = reducer(undefined, { type: ActionTypes.INIT })
if (typeof initialState === 'undefined') {
throw new Error(
`xxxx`
)
}
// 发出一个未知的action
// reducer需要返回原有的state 否则报错
if (
typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === 'undefined'
) {
throw new Error(
`xxx`
)
}
})
}
还是之前提到的对于Reducer函数的要求,Redux内部对reducer有严格的要求,这里就不再重复说明了。 方法的最后,返回了合成的reducer,我们来看看这个reducer是如何分发action和对state树执行数据处理逻辑的:
// 最终返回的 reducer(state,action)
return function combination(state = {}, action) {
let hasChanged = false // 状态树是否发生了变化
const nextState = {} // 状态树的下一次状态
/*
* 每个子reducer都会管理状态树的一部分状态
* dispatch发起一个action后 会分发给所有的子reducer执行
* 有需要的子reducer进行数据处理 返回新的state
* 不需要的返回原有的state
*/
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
// 当前reducer的state值
const previousStateForKey = state[key]
// 调用reducer函数 执行数据处理逻辑
// 返回处理后的 state
const nextStateForKey = reducer(previousStateForKey, action)
// 检查返回的state是否合法
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 子reducer是否对state树做了更新
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 只要有一个reducer修改了状态树,就返回新的状态树
return hasChanged ? nextState : state
}
总结
现在看来,combineReducers 是划分state树的数据处理逻辑给多个reducer实现并不复杂,每个reducer负责state树的一部分,当 dispatch 发起了一个action后,combination 会将这个action分发给所有的子 reducer,不处理此action的reducer返回原有状态就可以,只有处理此 action 的 reducer 才会返回更新后的部分state。
我们需要注意到的是,子reducer维护的那一部分state在全局state树中的命名,默认就是reducer的函数名,如有以下两个 reducer:
function todos(state = [] , action){
}
function visibilityFilter(state = SHOW_ALL, action) {
}
则合成之后的state树是这样的:
{
todos : [],
visibilityFilter: SHOW_ALL
}
如果要修改命名,则在调用 combineReducers 时,应该这样:
const todoApp= combineReducers({
anotherName: todos,
yetAnotherName: todoApp
})
拆分Reducer的好处是显而易见的,使Redux便于维护,同时代码更加清晰,但编写许多重复的reducer代码就会变得多余枯燥了。