The original link: blog.zebwu.com/2020/12/08/…

As Web applications become more complex, you need to split the REdux state, using a separate Reducer for each module. You can’t live without combineReducers.

Before reading the source code

As always, review the official documentation for the API before reading the source code.

CombineReducers is a Helper function that receives multiple Reducing Functions and returns a Reduced Reducer. The Resulting Reducer is executed by calling all child Reducer and returning their results as an object with a namespace determined for each state. The namespace depends on the key of the object passed to the combineReducers.

CombineReducers has only one parameter – reducers composed of objects.

CombineReducers implements some checking rules to reduce problems encountered during development:

  1. If you encounter an unexpected ActionType, you should return state as is.

  2. Never return undefined. CombineReducers throw an error in this case.

  3. If the Reducers received undefined state, you should return Initial State.

In addition, combineReducers checks your Reducer functions (passing undefined to them) even if you define initial state.

Again, we read the source code with questions in mind. From the above description, I have the following questions:

  1. combineReducersHow is namespace implemented?
  2. combineReducersHow is the check implemented?

With that in mind, we started reading the source code.

Start reading the source code

First, take a walk through the user-entered Reducers object. Make sure that each reducer has a corresponding reducer and make a copy to finalReducers. All subsequent operations will use the checked finalReducers.

Corresponding source code address.

export default function combineReducers(reducers: ReducersMapObject) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers: ReducersMapObject = {}
  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)
Copy the code

Then, the finalReducers pass undefined check. Save any errors caught, if any.

Corresponding source code address

let shapeAssertionError: Error
try {
  assertReducerShape(finalReducers)
} catch (e) {
  shapeAssertionError = e
}
Copy the code

Let’s look at the implementation of assertReducerShape.

Corresponding source code address

function assertReducerShape(reducers: ReducersMapObject) {
  // Iterate through the reducers
  Object.keys(reducers).forEach(key= > {
    const reducer = reducers[key]
    // Pass undefined to the reducers to see if it returns Initial state
    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 you want to set a value to null, you should set it to null instead of undefined)}Reducer unknown action to see if it returns state directly
    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.`)}}}Copy the code

As commented in the code. AssertReducerShape traverses the reducers and checks whether each Reducer returns initial state when init and directly returns state when processing unknown actions.

CombineReducers then returns a function combination. Let’s start with some error handling.

Corresponding source code address

return function combination(
	state: StateFromReducersMapObject<typeof reducers> = {},
  action: AnyAction
	) {
  // Throws shapeAssertionError, if any
  if (shapeAssertionError) {
    throw shapeAssertionError
  }
	// In a non-production environment warning "an inconsistency with the expected state structure has been obtained"
  if(process.env.NODE_ENV ! = ='production') {
    const warningMessage = getUnexpectedStateShapeWarningMessage(
      state,
      finalReducers,
      action,
      unexpectedKeyCache
    )
    if (warningMessage) {
      warning(warningMessage)
    }
  }
Copy the code

ShapeAssertionError needs no further elaboration. As for getUnexpectedStateShapeWarningMessage, first take a look at its implementation.

The corresponding source address

function getUnexpectedStateShapeWarningMessage(
  inputState: object,
  reducers: ReducersMapObject,
  action: Action,
  unexpectedKeyCache: { [key: string] :true }
) {
  const reducerKeys = Object.keys(reducers) / / reducer namespace
  // The source of the problem, based on the type of action
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer' 
	// If no reducer is available
  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.')}// Check whether it is "pure object"
  if(! isPlainObject(inputState)) {const match = Object.prototype.toString // Notice the way 'toString()' is called here
      .call(inputState)
      .match(/\s([a-z|A-Z]+)/)
    const matchType = match ? match[1] : ' '
    return (
      `The ${argumentName} has unexpected type of "` +
      matchType +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('",")}"`)}// Check for additional attributes in inputState
  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
	// Return error
  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.`)}}Copy the code

Determine if it is a “pure object” and check for additional attributes. The error information is displayed. Refer to the comments for implementation details.

Finally, the state was given to each Reducer and the new nextState was obtained. And check that nextStateForKey is not undefined during the process. Returns the final state. The code is relatively simple and not difficult to read.

The corresponding source address

    let hasChanged = false // Whether to change the flag bit
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    // Walk through all the reducers
    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) NextState was obtained from the reducer calculation
      / / undefined processing
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // See if state changes and use short-circuit optimizationhasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged =// The value does not change, but parts of the impure object are removedhasChanged || finalReducerKeys.length ! = =Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
Copy the code

Reference

  • combineReducers | Redux

  • redux – GitHub