前面的两个专栏我们分别对 Redux 应用中的唯一数据状态存储仓库 Store 以及其所存储的数据状态 State发起修改数据状态的 Action 以及避免冗余代码的 Action Creator 进行了详细的了解。在这个过程中,我们了解到在 Redux 中唯一修改数据状态 State 的方式是通过 dispatch 发送一个 action 到 reducer,然后在 reducer 中根据 action 的描述,对数据状态 State 做相应的修改,并返回一个新的 State。下面还是那张我们熟悉的 Redux 数据流程图:
image.png

Reducer

还记得我们在 Redux 系列二:Store 与 State 说到的关于 createStore 函数的源码吗?如果你忘了,可以点击链接回过头在温故一下:

  1. export default function createStore(reducer, preloadedState, enhancer) {
  2. // do something
  3. }

createStore 函数会接受一到三个参数,第一个描述 State 修改逻辑的 reducer 函数是必传的,不然整个 Store 会无效:

  1. if (typeof reducer !== 'function') {
  2. throw new Error('Expected the reducer to be a function.')
  3. }

在前面的内容中,我们也经常提到:reducer 是唯一修改 State 的逻辑,它会接受当前的 State 和修改 State 的动作 action 返回新的数据状态 newState,可以用伪代码表述为:

  1. const reducer = (prevState, action) => {
  2. // do something
  3. return newState
  4. }

对于 reducer 的基础概念有了认识,我们就来结合实际的场景说一下吧!

todo list 大家都应该很清楚吧,我们就说一下关于 todo list 的增删改查,用 reducer 大致可以描述成这样:

  1. // reducer 逻辑
  2. const reducer = (state = [], action) => {
  3. switch (action.type) {
  4. case ADD_TODO:
  5. return [
  6. ...state,
  7. {
  8. text: action.text,
  9. completed: false
  10. }
  11. ]
  12. case TOGGLE_TODO:
  13. return state.map((todo, index) => {
  14. if (index === action.index) {
  15. return Object.assign({}, todo, {
  16. completed: !todo.completed
  17. })
  18. }
  19. return todo
  20. })
  21. default:
  22. return state
  23. }
  24. }

reducer 的主逻辑是 switch case,我们会用 action.type 进行 case 条件匹配,然后在每个 case 分支返回新的数据状态 State,注意是返回新的 State 而不是在旧的 State 上进行修改,关于实现你可以借用扩展运算符(spread)和 Object.assign。上面 reducer 的逻辑都很简单,我就不做过多的介绍了。

不知道大家有没有注意到,我们一直在强调在 reducer 逻辑中,我们希望每次的逻辑是返回新的 State 而不是在旧的 State 上进行修改,这就是 reducer 的一个很强大的特性了:传函数。Redux 的作者希望开发者在设计 reducer 的时候,将它设计成一个纯函数,即有相同的输入即有相同的输出,这样在数据状态追踪方面有极大的好处。因为,每次都是生成新的数据状态 State,所以我们可以将每个阶段生成的 State 保存起来做回放或前进等方便问题追踪的功能和效果,还保证了 view 对应的 State 总是一个不变的对象。

Reducer 拆分与合并

上面我们给出的 reducer 的示例代码是极其简单的,但是在实际的业务场景开发中可能会有几十上百的页面逻辑,甚至更多。对于这么复杂的业务场景,State 对象肯定会很庞大,所有修改 reducer 的逻辑肯定不能写在一个 reducer 函数中,所以为了更好的进行管理我们可以对 reducer 进行拆分。比如有这样一个场景,在没进行拆分之前的代码是这样的:

  1. const chatReducer = (state = defaultState, action = {}) => {
  2. const { type, payload } = action;
  3. switch (type) {
  4. case ADD_CHAT:
  5. return Object.assign({}, state, {
  6. chatLog: state.chatLog.concat(payload)
  7. })
  8. case CHANGE_STATUS:
  9. return Object.assign({}, state, {
  10. statusMessage: payload
  11. })
  12. case CHANGE_USERNAME:
  13. return Object.assign({}, state, {
  14. userName: payload
  15. })
  16. default:
  17. return state
  18. }
  19. }

我们不难看出,上面的示例代码糅杂了三个不同的业务场景:

  1. // chatLog属性
  2. ADD_CHAT
  3. // statusMessage属性
  4. CHANGE_STATUS
  5. // userName属性
  6. CHANGE_USERNAME

这三个属性之间没有任何的联系,我们完全可以把 reducer 函数进行拆分,使不同的函数负责处理不同属性,最后再把它们合并成一个大的 reducer 即可:

  1. const chatReducer = (state = defaultState, action = {}) => {
  2. return {
  3. chatLog: chatLog(state.chatLog, action),
  4. statusMessage: statusMessage(state.statusMessage, action),
  5. userName: userName(state.userName, action)
  6. }
  7. }

在这里,我们把上面那个大的 reducer 拆分成了三个小函数,让他们各自负责对应的状态属性。经过拆分后的 reducer 变得好读好管理多了,也和 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 reducer 完全可以对应。

为了更方便我们对 reducer 的拆分和合并,Redux 提供了 combineReducers 函数,你只要定义各个子 reducer 函数,然后用这个方法将它们合成一个大的 reducer。

  1. import { combineReducers } from 'redux'
  2. const chatReducer = combineReducers({
  3. chatLog,
  4. statusMessage,
  5. userName
  6. })

这种写法有一个前提,就是 State 的属性名必须与子 reducer 同名,如果不同名,就要采用下面的写法:

  1. const reducer = combineReducers({
  2. a: doSomethingWithA,
  3. b: processB,
  4. c: c
  5. })
  6. // 等同于
  7. function reducer(state = {}, action) {
  8. return {
  9. a: doSomethingWithA(state.a, action),
  10. b: processB(state.b, action),
  11. c: c(state.c, action)
  12. }
  13. }

总之,combineReducers() 做的就是根据各个小的 reducer 函数产生一个整体的 reducer 函数。该函数会根据 State 的 key 去执行相应的子 reducer,并将返回结果合并成一个大的 State 对象。

combineReducers

combineReducers 函数说到这里,我们可以推导一下函数的具体实现了。

我们说过 combineReducers 函数是用来将多个小的 reducer 合并成一个大的 reducer 的,所以首先 combineReducers 函数会接受多个小 reducer 函数的集合,形如前面示例代码提到的:

  1. {
  2. chatLog,
  3. statusMessage,
  4. userName
  5. }

然后函数的执行会生成一个大的 reducer,而 reducer 又是一个函数类型,所以最终 combineReducers 函数的调用会返回一个函数。reducer 函数会接受当前的 state 和描述动作的 action 返回新的 state,所以 combineReducers 函数返回的函数会接受两个参数:一个是 state,另一个是 action。大致就应该是这样了:

  1. const combineReducers = reducers => {
  2. return (state = {}, action) => {
  3. // do something
  4. }
  5. }

接着我们继续挖这个 do something 的逻辑。既然是进行多个 reducer 的合并,那 do something 的逻辑肯定是根据传入的 state 和 action 执行每个 reducer,然后将每个 reducer 返回的新的 state 进行合并,组成大的 state,最终的代码如下:

  1. const combineReducers = reducers => {
  2. return (state = {}, action) => {
  3. return Object.keys(reducers).reduce(
  4. (nextState, key) => {
  5. nextState[key] = reducers[key](state[key], action);
  6. return nextState;
  7. },
  8. {}
  9. )
  10. }
  11. }

说了这么多,看看 Redux 中 combineReducers 函数的实现吧!删除一些注释和异常处理的代码,展示如下:

  1. export default function combineReducers(reducers) {
  2. const reducerKeys = Object.keys(reducers)
  3. const finalReducers = {}
  4. for (let i = 0; i < reducerKeys.length; i++) {
  5. const key = reducerKeys[i]
  6. if (typeof reducers[key] === 'function') {
  7. finalReducers[key] = reducers[key]
  8. }
  9. }
  10. const finalReducerKeys = Object.keys(finalReducers)
  11. return function combination(state = {}, action) {
  12. let hasChanged = false
  13. const nextState = {}
  14. for (let i = 0; i < finalReducerKeys.length; i++) {
  15. const key = finalReducerKeys[i]
  16. const reducer = finalReducers[key]
  17. const previousStateForKey = state[key]
  18. const nextStateForKey = reducer(previousStateForKey, action)
  19. if (typeof nextStateForKey === 'undefined') {
  20. const errorMessage = getUndefinedStateErrorMessage(key, action)
  21. throw new Error(errorMessage)
  22. }
  23. nextState[key] = nextStateForKey
  24. hasChanged = hasChanged || nextStateForKey !== previousStateForKey
  25. }
  26. return hasChanged ? nextState : state
  27. }
  28. }

其实基础逻辑和我们上面分析的一致,接受多个 reducer 的集合,返回一个接受 state 和 action 的函数,在函数逻辑中根据传入的 state 和 action 执行每个 reducer 函数,最后将每个 reducer 函数返回的新的 state 进行合并,返回大的 state 集合。

总结

说到这里,关于 reducer 的知识就讲完了。专栏开头,我们根据前面讲到的 Store、state 和 action 将 reducer 的话题引出来,然后讲到了 reducer 函数的基础逻辑设计,大型应用中 reducer 的拆分与合并,最后讲到了 reducer 合并函数 combineReducers 的实现与源码解析。还有关于 reducer 在一些特殊业务场景下需要替换的逻辑 replaceReducer 函数,由于在 Redux 系列二:Store 与 State 专栏已经讲到了,这里就不在赘述。关于后面一个专栏的内容,我希望将前面这几个专栏的知识点串起来,所以会结合 React 和 Redux 实现 todo list 的功能。