逐行阅读redux源码(二)combineReducers

2,326 阅读6分钟

前情提要

认识reducers

在我们开始学习源码之前,我们不妨先来看看何谓reducers:

image

如图所见,我们可以明白, reducer 是用来对初始的状态树进行一些处理从而获得一个新的状态树的,我们可以继续从其使用方法看看 reducer 到底如何做到这一点:

function reducerDemo(state = {}, action) {
  switch (action.type) {
    case 'isTest':
      return {
        isTest: true
      };
    default:
      return state;
  }
}

从我们的 reducerDemo 中,我们可以看到 reducer 接受了两个参数:

  • state
  • action

通过对 action 中的 type 的判断,我们可以用来确定当前 reducer 是对指定 typeaction 进行响应,从而对初始的 state 进行一些修改,获得修改之后的 state 的。从之前我们在 createStore 中看到的情况:

currentState = currentReducer(currentState, action)

每次 reducer 都会使用上一次的 state,然后处理之后获得新的 state

但是光是如此的话,在处理大型项目的时候我们似乎有点捉襟见肘,因为一个store只能接受一个reducer,在大型项目中我们通常会有非常非常多的 action 用来对状态树进行修改,当然你也可以在 reducer 中声明海量的 switch...case.. 来实现对单个action的响应修改,但是当你这样做的时候,你会发现你的reducer越来越大,处理过程越来越复杂,各个业务逻辑之间的耦合度越来越高,最后你就会发现这个 reducer 将完全无法维护。

所以为了解决在大型项目中的这类问题,我们会使用多个reducer,每个reducer会去维护自己所属的单独业务,但是正如我们之前所说,每个store只会接受一个 reducer,那我们是如何将reducer1、reducer2、reducer3、reducer4整合成一个reducer并且返回我们所需的状态树的呢?

combineReducers

当然我们能想到的问题,redux 肯定也能想到,所以他们提供了 combineReducers api让我们可以将多个 reducer 合并成一个 reducer ,并根据对应的一些规则生成完整的状态树,so,让我们进入正题,开始阅读我们 combineReducers 的源码吧:

依赖

首先是combineReducers的依赖,我们能在代码的头部找到它:

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'

可以看到,combineReducers仅仅依赖了之前我们在上一篇文章中提到的工具类:

  • ActionTypes(内置的actionType)
  • warning(显式打印错误)
  • isPlainObject(检测是否为对象)

错误信息处理

进入正文,在combineReducers的开始部分,我们能够发现许多用于返回错误信息的方法:

  • getUndefinedStateErrorMessage(当reducer返回一个undefined值时返回的错误信息)
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action'

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

从方法可知,这个处理过程中,我们传入了key(reducer的方法名)以及action对象,之后根据action中是否存在type获得了action的描述,最终返回了一段关于出现返回undefined值的reduceraction的描述语以及提示。

  • getUnexpectedStateShapeWarningMessage(获取当前state中存在的没有reducer处理的状态的提示信息)
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
}

在说这段源码之前,我们需要稍微了解一下,当我们使用combineReucers,我们传入的reducer的数据结构:

function reducer1(state={}, action) {
    switch (action.type) {
    case 'xxx':
      return true;
    default:
      return state;
  }
}

function reducer2() {...}
function reducer3() {...}
function reducer4() {...}

const rootReducer = combineReucers({
    reducer1,
    reducer2,
    reducer3,
    reducer4
})

我们传入的时以reducer的方法名作为键,以其函数作为值的对象,而使用rootReducer生成的store会是同样以每个reducer的方法名作为键,其reducer处理之后返回的state作为值的对象,比如:

// 生成的state
{
    reducer1: state1,
    reducer2: state2,
    reducer3: state3,
    reducer4: state4
}

至于为何会这样,我们后面再提,现在先让我们继续往下阅读这个生成错误信息的方法。

在这个方法中,其工作流程大概如下:

  • 声明reducerKeys获取当前合并的reducer的所有键值
  • 声明argumentName获取当前是否为第一次初始化store的描述
  • 当不存在reducer的时候返回抛错信息
  • 当传入的state不是一个对象时,返回报错信息。
  • 获取state上未被reducer处理的状态的键值unexpectedKeys,并将其存入cache值中。
  • 检测是否为内置的replace action,因为当使用storereplaceReducer时会自动触发该内置action,并将reducer替换成传入的,此时检测的reducer和原状态树必然会存在冲突,所以在这种情况下检测到的unexpectedKeys并不具备参考价值,将不会针对性的返回抛错信息,反之则会返回。

通过如上流程,我们将能对未被reducer处理的状态进行提示。

  • assertReducerShape(检测reducer是否符合使用规则)
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === 'undefined'
    ) {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

相对之前的多次判断,这个就要简单暴力的多了,直接遍历所有的reducer,首先通过传入undefined的初始值和内置的init action,如果不能返回正确的值(返回了undefined值),那么说明reducer并没有针对默认属性返回正确的值,我们将提供指定的报错信息。

这之后又使用reducer处理了undefined初始值和内置随机action的情况,这一步的目的是为了排除用户为了避免第一步的判断,从而手动针对内置init action进行处理,如果用户确实做了这种处理,就抛出对应错误信息。

如此,我们对combineReucers的错误信息处理已经有了大概的了解,其大致功能如下:

  • 判断reducer是否是合规的
  • 找出哪些reducer不合规
  • 判断状态树上有哪些没有被reducer处理的状态

了解了这些之后,我们便可以进入真正的combineReducers了。

合并reducers

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

首先我们看到变量声明部分:

  • reducerKeys (reducer在对象中的方法名)
  • finalReducers (最终合并生成的的reducers)

接下来,该方法循环遍历了reducerKeys,并在产品级(production)环境下对类型为undefinedreducer进行了过滤和打印警告处理,其后又将符合规范的reducer放到了finalReducer中,这一步是为了尽量减少后面的流程受到空值reducer的影响。

然后combineReducers进一步的对这些非空reducer进行了处理,检测其中是否还有不合规范的reducer(通过assertReducerShape),并通过try catch 将这个错误存储到shapeAssertionError变量中。

正如我们一直所说,reducer需要是一个function,所以我们的combineReducer将是一个高阶函数,其会返回一个新的reducer,也就是源码中的combination

在返回的combination中,会检测是否有shapeAssertionError,如果有调用该reducer时将终止当前流程,抛出一个错误,并且在产品级环境下,还会检测是否有未被reducer处理的state并打印出来进行提示(不中断流程)。

最后才是整个combination的核心部分,首先其声明了一个变量来标识当前状态树是否更改,并声明了一个空的对象用来存放接下来会发生改变的状态,然后其遍历了整个finalReducer,通过每个reducer处理当前state,并将其获得的每个值和之前状态树中的对应key值得状态值进行对比,如果不一致,那么就更新hasChanged状态,并将新的状态值放到指定key值得state中,且更新整个状态树,当然其中还是会对出现异常state返回值的异常处理。

结语

到此,我们已经通读了combineReducers中的所有代码,也让我们稍微对使用combineReducer时需要注意的几个点做一个总结:

  • 每个reducer必须要有非undefined的返回值
  • 不要使用reducer手动去操作内置的action
  • combineReducers需要注意传入的对象每个键必须对应一个类型为functionreducer(废话

请大家记住这几个点,在这些前提下能够帮助你更快的理解我们的combineReducers

感谢你的阅读~