读Redux源码04- combineReducers

361 阅读4分钟

前面说到,当你的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代码就会变得多余枯燥了。