§ combineReducers(reducers)

⊙ 应用场景

简明教程中的 code-7 如下:

  1. /** 本代码块记为 code-7 **/
  2. var initState = {
  3. counter: 0,
  4. todos: []
  5. }
  6. function reducer(state, action) {
  7. if (!state) state = initState
  8. switch (action.type) {
  9. case 'ADD_TODO':
  10. var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
  11. nextState.todos.push(action.payload)
  12. return nextState
  13. default:
  14. return state
  15. }
  16. }

上面的 reducer 仅仅是实现了 “新增待办事项” 的 state 的处理
我们还有计数器的功能,下面我们继续增加计数器 “增加 1” 的功能:

  1. /** 本代码块记为 code-8 **/
  2. var initState = { counter: 0, todos: [] }
  3. function reducer(state, action) {
  4. if (!state) return initState // 若是初始化可立即返回应用初始状态
  5. var nextState = _.cloneDeep(state) // 否则二话不说先克隆
  6. switch (action.type) {
  7. case 'ADD_TODO': // 新增待办事项
  8. nextState.todos.push(action.payload)
  9. break
  10. case 'INCREMENT': // 计数器加 1
  11. nextState.counter = nextState.counter + 1
  12. break
  13. }
  14. return nextState
  15. }

如果说还有其他的动作,都需要在 code-8 这个 reducer 中继续堆砌处理逻辑
但我们知道,计数器 与 待办事项 属于两个不同的模块,不应该都堆在一起写
如果之后又要引入新的模块(例如留言板),该 reducer 会越来越臃肿
此时就是 combineReducers 大显身手的时刻:

  1. 目录结构如下
  2. reducers/
  3. ├── index.js
  4. ├── counterReducer.js
  5. ├── todosReducer.js
  1. /** 本代码块记为 code-9 **/
  2. /* reducers/index.js */
  3. import { combineReducers } from 'redux'
  4. import counterReducer from './counterReducer'
  5. import todosReducer from './todosReducer'
  6. const rootReducer = combineReducers({
  7. counter: counterReducer, // 键名就是该 reducer 对应管理的 state
  8. todos: todosReducer
  9. })
  10. export default rootReducer
  11. -------------------------------------------------
  12. /* reducers/counterReducer.js */
  13. export default function counterReducer(counter = 0, action) { // 传入的 state 其实是 state.counter
  14. switch (action.type) {
  15. case 'INCREMENT':
  16. return counter + 1 // counter 是值传递,因此可以直接返回一个值
  17. default:
  18. return counter
  19. }
  20. }
  21. -------------------------------------------------
  22. /* reducers/todosReducers */
  23. export default function todosReducer(todos = [], action) { // 传入的 state 其实是 state.todos
  24. switch (action.type) {
  25. case 'ADD_TODO':
  26. return [ ...todos, action.payload ]
  27. default:
  28. return todos
  29. }
  30. }

code-8 reducercode-9 rootReducer 的功能是一样的,但后者的各个子 reducer 仅维护对应的那部分 state
其可操作性、可维护性、可扩展性大大增强

Flux 中是根据不同的功能拆分出多个 store 分而治之
而 Redux 只允许应用中有唯一的 store,通过拆分出多个 reducer 分别管理对应的 state


下面继续来深入使用 combineReducers。一直以来我们的应用状态都是只有两层,如下所示:

  1. state
  2. ├── counter: 0
  3. ├── todos: []

如果说现在又有一个需求:在待办事项模块中,存储用户每次操作(增删改)的时间,那么此时应用初始状态树应为:

  1. state
  2. ├── counter: 0
  3. ├── todo
  4. ├── optTime: []
  5. ├── todoList: [] # 这其实就是原来的 todos!

那么对应的 reducer 就是:

  1. 目录结构如下
  2. reducers/
  3. ├── index.js <-------------- combineReducers (生成 rootReducer)
  4. ├── counterReducer.js
  5. ├── todoReducers/
  6. ├── index.js <------ combineReducers
  7. ├── optTimeReducer.js
  8. ├── todoListReducer.js
  1. /* reducers/index.js */
  2. import { combineReducers } from 'redux'
  3. import counterReducer from './counterReducer'
  4. import todoReducers from './todoReducers/'
  5. const rootReducer = combineReducers({
  6. counter: counterReducer,
  7. todo: todoReducers
  8. })
  9. export default rootReducer
  10. =================================================
  11. /* reducers/todoReducers/index.js */
  12. import { combineReducers } from 'redux'
  13. import optTimeReducer from './optTimeReducer'
  14. import todoListReducer from './todoListReducer'
  15. const todoReducers = combineReducers({
  16. optTime: optTimeReducer,
  17. todoList: todoListReducer
  18. })
  19. export default todoReducers
  20. -------------------------------------------------
  21. /* reducers/todosReducers/optTimeReducer.js */
  22. export default function optTimeReducer(optTime = [], action) {
  23. // 咦?这里怎么没有 switch-case 分支?谁说 reducer 就一定包含 switch-case 分支的?
  24. return action.type.includes('TODO') ? [ ...optTime, new Date() ] : optTime
  25. }
  26. -------------------------------------------------
  27. /* reducers/todosReducers/todoListReducer.js */
  28. export default function todoListReducer(todoList = [], action) {
  29. switch (action.type) {
  30. case 'ADD_TODO':
  31. return [ ...todoList, action.payload ]
  32. default:
  33. return todoList
  34. }
  35. }

无论您的应用状态树有多么的复杂,都可以通过逐层下分管理对应部分的 state

  1. counterReducer(counter, action) -------------------- counter
  2. rootReducer(state, action) —→∑ optTimeReducer(optTime, action) ------ optTime nextState
  3. ↘—→∑ todo
  4. todoListReducer(todoList,action) ----- todoList
  5. 注:左侧表示 dispatch 分发流,∑ 表示 combineReducers;右侧表示各实体 reducer 的返回值,最后汇总整合成 nextState

看了上图,您应该能直观感受到为何取名为 reducer 了吧?把 state 分而治之,极大减轻开发与维护的难度

无论是 dispatch 哪个 action,都会流通所有的 reducer
表面上看来,这样子很浪费性能,但 JavaScript 对于这种纯函数的调用是很高效率的,因此请尽管放心
这也是为何 reducer 必须返回其对应的 state 的原因。否则整合状态树时,该 reducer 对应的键值就是 undefined

⊙ 源码分析

仅截取关键部分,毕竟有很大一部分都是类型检测警告

  1. function combineReducers(reducers) {
  2. var reducerKeys = Object.keys(reducers)
  3. var finalReducers = {}
  4. for (var i = 0; i < reducerKeys.length; i++) {
  5. var key = reducerKeys[i]
  6. if (typeof reducers[key] === 'function') {
  7. finalReducers[key] = reducers[key]
  8. }
  9. }
  10. var finalReducerKeys = Object.keys(finalReducers)
  11. // 返回合成后的 reducer
  12. return function combination(state = {}, action) {
  13. var hasChanged = false
  14. var nextState = {}
  15. for (var i = 0; i < finalReducerKeys.length; i++) {
  16. var key = finalReducerKeys[i]
  17. var reducer = finalReducers[key]
  18. var previousStateForKey = state[key] // 获取当前子 state
  19. var nextStateForKey = reducer(previousStateForKey, action) // 执行各子 reducer 中获取子 nextState
  20. nextState[key] = nextStateForKey // 将子 nextState 挂载到对应的键名
  21. hasChanged = hasChanged || nextStateForKey !== previousStateForKey
  22. }
  23. return hasChanged ? nextState : state
  24. }
  25. }

在此我的注释很少,因为代码写得实在是太过明了了,注释反而影响阅读
作者 Dan 用了大量的 for 循环,的确有点不够优雅