§ combineReducers(reducers)
⊙ 应用场景
简明教程中的 code-7
如下:
/** 本代码块记为 code-7 **/
var initState = {
counter: 0,
todos: []
}
function reducer(state, action) {
if (!state) state = initState
switch (action.type) {
case 'ADD_TODO':
var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
nextState.todos.push(action.payload)
return nextState
default:
return state
}
}
上面的 reducer
仅仅是实现了 “新增待办事项” 的 state
的处理
我们还有计数器的功能,下面我们继续增加计数器 “增加 1” 的功能:
/** 本代码块记为 code-8 **/
var initState = { counter: 0, todos: [] }
function reducer(state, action) {
if (!state) return initState // 若是初始化可立即返回应用初始状态
var nextState = _.cloneDeep(state) // 否则二话不说先克隆
switch (action.type) {
case 'ADD_TODO': // 新增待办事项
nextState.todos.push(action.payload)
break
case 'INCREMENT': // 计数器加 1
nextState.counter = nextState.counter + 1
break
}
return nextState
}
如果说还有其他的动作,都需要在 code-8
这个 reducer
中继续堆砌处理逻辑
但我们知道,计数器 与 待办事项 属于两个不同的模块,不应该都堆在一起写
如果之后又要引入新的模块(例如留言板),该 reducer
会越来越臃肿
此时就是 combineReducers
大显身手的时刻:
目录结构如下
reducers/
├── index.js
├── counterReducer.js
├── todosReducer.js
/** 本代码块记为 code-9 **/
/* reducers/index.js */
import { combineReducers } from 'redux'
import counterReducer from './counterReducer'
import todosReducer from './todosReducer'
const rootReducer = combineReducers({
counter: counterReducer, // 键名就是该 reducer 对应管理的 state
todos: todosReducer
})
export default rootReducer
-------------------------------------------------
/* reducers/counterReducer.js */
export default function counterReducer(counter = 0, action) { // 传入的 state 其实是 state.counter
switch (action.type) {
case 'INCREMENT':
return counter + 1 // counter 是值传递,因此可以直接返回一个值
default:
return counter
}
}
-------------------------------------------------
/* reducers/todosReducers */
export default function todosReducer(todos = [], action) { // 传入的 state 其实是 state.todos
switch (action.type) {
case 'ADD_TODO':
return [ ...todos, action.payload ]
default:
return todos
}
}
code-8 reducer
与 code-9 rootReducer
的功能是一样的,但后者的各个子 reducer
仅维护对应的那部分 state
其可操作性、可维护性、可扩展性大大增强
Flux 中是根据不同的功能拆分出多个
store
分而治之
而 Redux 只允许应用中有唯一的store
,通过拆分出多个reducer
分别管理对应的state
下面继续来深入使用 combineReducers
。一直以来我们的应用状态都是只有两层,如下所示:
state
├── counter: 0
├── todos: []
如果说现在又有一个需求:在待办事项模块中,存储用户每次操作(增删改)的时间,那么此时应用初始状态树应为:
state
├── counter: 0
├── todo
├── optTime: []
├── todoList: [] # 这其实就是原来的 todos!
那么对应的 reducer
就是:
目录结构如下
reducers/
├── index.js <-------------- combineReducers (生成 rootReducer)
├── counterReducer.js
├── todoReducers/
├── index.js <------ combineReducers
├── optTimeReducer.js
├── todoListReducer.js
/* reducers/index.js */
import { combineReducers } from 'redux'
import counterReducer from './counterReducer'
import todoReducers from './todoReducers/'
const rootReducer = combineReducers({
counter: counterReducer,
todo: todoReducers
})
export default rootReducer
=================================================
/* reducers/todoReducers/index.js */
import { combineReducers } from 'redux'
import optTimeReducer from './optTimeReducer'
import todoListReducer from './todoListReducer'
const todoReducers = combineReducers({
optTime: optTimeReducer,
todoList: todoListReducer
})
export default todoReducers
-------------------------------------------------
/* reducers/todosReducers/optTimeReducer.js */
export default function optTimeReducer(optTime = [], action) {
// 咦?这里怎么没有 switch-case 分支?谁说 reducer 就一定包含 switch-case 分支的?
return action.type.includes('TODO') ? [ ...optTime, new Date() ] : optTime
}
-------------------------------------------------
/* reducers/todosReducers/todoListReducer.js */
export default function todoListReducer(todoList = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [ ...todoList, action.payload ]
default:
return todoList
}
}
无论您的应用状态树有多么的复杂,都可以通过逐层下分管理对应部分的 state
:
counterReducer(counter, action) -------------------- counter
↗ ↘
rootReducer(state, action) —→∑ ↗ optTimeReducer(optTime, action) ------ optTime ↘ nextState
↘—→∑ todo ↗
↘ todoListReducer(todoList,action) ----- todoList ↗
注:左侧表示 dispatch 分发流,∑ 表示 combineReducers;右侧表示各实体 reducer 的返回值,最后汇总整合成 nextState
看了上图,您应该能直观感受到为何取名为 reducer
了吧?把 state
分而治之,极大减轻开发与维护的难度
无论是
dispatch
哪个action
,都会流通所有的reducer
表面上看来,这样子很浪费性能,但 JavaScript 对于这种纯函数的调用是很高效率的,因此请尽管放心
这也是为何reducer
必须返回其对应的state
的原因。否则整合状态树时,该reducer
对应的键值就是undefined
⊙ 源码分析
仅截取关键部分,毕竟有很大一部分都是类型检测警告
function combineReducers(reducers) {
var reducerKeys = Object.keys(reducers)
var finalReducers = {}
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
var finalReducerKeys = Object.keys(finalReducers)
// 返回合成后的 reducer
return function combination(state = {}, action) {
var hasChanged = false
var nextState = {}
for (var i = 0; i < finalReducerKeys.length; i++) {
var key = finalReducerKeys[i]
var reducer = finalReducers[key]
var previousStateForKey = state[key] // 获取当前子 state
var nextStateForKey = reducer(previousStateForKey, action) // 执行各子 reducer 中获取子 nextState
nextState[key] = nextStateForKey // 将子 nextState 挂载到对应的键名
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
在此我的注释很少,因为代码写得实在是太过明了了,注释反而影响阅读
作者 Dan 用了大量的for
循环,的确有点不够优雅