Combine Reducers for Reux Source Analysis

Keywords: Attribute

Preface: The last article finished the basic core part of redux. We know that in the actual development, we will split reducer, so as to facilitate code reading and maintenance, data management is more clear. From the previous article, we know that createStore can only accept a reducer as a parameter. If we split the reducer, how can we pass multiple reducers into the createStore function? Next, let's talk about the role of combineReducers.

First, let's look at the use of combineReducers

//reducer for managing user information
function userReducer(state={},action) {
    return state
}
//reducer for Managing Menu Information
function menusReducer(state=[],action) {
    return state
}
//reducer merge
const reducers = combineReducers({userReducer,menusReducer})
//This is an abbreviation of the ES6 object, equivalent to
//{userReducer:userReducer,menusReducer:menusReducer}
//Create a store
const store = createStore(reducers)

Now let's look at what combineReducers do internally. The source code for combineReducers.js is as follows

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

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.`
    )
}

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.`
        )
    }
}

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.`
            )
        }
    })
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */
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
    }
}

Let's look directly at the combinReducers function, which accepts only one parameter, reducers. What is this reducers? According to our example above, the value of this reducers is

{
    userReducer:function(){...},
    menusReducers:function(){...}
}

Next, let's look at the implementation inside the function.

 //Gets the key name of the incoming object and returns an array of key names
 const reducerKeys = Object.keys(reducers)
 //Declare an empty object to save the final reducers
 const finalReducers = {}

At this point, the value of reducerKeys is an array of ["userReducer","menusReducer"]

//Traversing reducerKeys
for (let i = 0; i < reducerKeys.length; i++) {
    //Get each key name
    const key = reducerKeys[i]
    //If it's in the development environment, make a judgment, and if reducer is undefined, an error message is prompted.
    //For example, when we call combineReducers, we pass in an object like {user:undefined}.
    if (process.env.NODE_ENV !== 'production') {
        if (typeof reducers[key] === 'undefined') {
            warning(`No reducer provided for key "${key}"`)
        }
    }
    //Determine whether the value of the corresponding key of the incoming object is a function type
    if (typeof reducers[key] === 'function') {
        //If so, put it in final Reducers
        finalReducers[key] = reducers[key]
    }
}

After the above traversal, the incoming reducers are copied to final Reducers, which is equivalent to a deep copy.

//Get an array of key names for finalReducers objects
const finalReducerKeys = Object.keys(finalReducers)

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

//Declare a variable
let shapeAssertionError
try {
    //Call the assertReducerShape function and pass in the finalReducers object
    assertReducerShape(finalReducers)
} catch (e) {
    shapeAssertionError = e
}

The above code first obtains the key name of the finalReducers object and saves it as an array; declares a variable unexpectedKeyCache and assigns its value to an empty object if it is a development environment. Next, a variable shape Assertion Error is declared to save error information. Then a function is called, finalReducers is passed in as a parameter, and the call of this function is wrapped up in try.. catch. When there is an error, the error message is assigned to shape Assertion Error. So we know that the call of this function may throw an error message. So we guess that the function is used for F. The data structure of inalReducers objects is checked, that is, we start calling the object passed in by combineReducers.

The source code of assertReducerShape function is as follows:

function assertReducerShape(reducers) {
    //Get the keys of reducers, get an array, and then traverse the array
    Object.keys(reducers).forEach(key => {
        //Get the reducer corresponding to each key
        const reducer = reducers[key]
        //Call reducer to get the initialization state, where our first parameter is passed in undefined and the second is an action.
        //The type type type is a random string, as I mentioned in the previous article. The purpose of this is to get reducer to return to the current state.
        //The current incoming state, in this case undefined
        const initialState = reducer(undefined, { type: ActionTypes.INIT })
        //Determine whether the value of initial state is undefined and if so, report an error
        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.`
            )
        }
        /**
         * Here, many people may doubt that the incoming state is undefined, and when executed, the default return value is returned directly to the state.
         * Isn't this initial state undefined? For example, our userReducer is as follows
         * function userReducer(state={},action) {return state}
         * We give state a default value when we declare, so when we pass undefined, we actually get {} empty objects, so here we go
         * The purpose is that when we declare reducer, state must give a default value. Or when you call createStore
         * preloadedState Parameter, which is the initial value of state
         */

        /**
         * Similar to the above, undefined and a random action.type are passed in to determine whether the return value of reducer is undefined.
         * For any action you disptach, if the type is not named in reducer, it should return to the current state, and the current state
         * state It can't be undefined, but it can be null
         */
        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.`
            )
        }
    })
}

Next, let's look at the last piece of code. We know that the first parameter reducer accepted by createStore must be a function, so the return value of combineReducer must be a function.

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
}

The first judgment has been mentioned above, so let's look at the role of the second if judgment.

function getUnexpectedStateShapeWarningMessage(
    inputState,
    reducers,
    action,
    unexpectedKeyCache
) {
    //Gets an array of key s for the reducers object
    const reducerKeys = Object.keys(reducers)
    //Determine the type of action, if it is ActionTypes.INIT, which means initialization, then state is called when createStore is called.
    //Initialization value of the incoming preloadedState. Otherwise, the last state value passed in through reducer
    const argumentName =
        action && action.type === ActionTypes.INIT
            ? 'preloadedState argument passed to createStore'
            : 'previous state received by the reducer'
    //If the length of the reducerKeys array is 0, a string describing the error is returned, that is, the object we pass in to combineReducers is an empty object {}
    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.'
        )
    }
    //To determine the state type, you must be a pure object
    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('", "')}"`
        )
    }
    //Traversing through the state to get an array of keys that are not expected
    const unexpectedKeys = Object.keys(inputState).filter(
        //When the key is not state itself, it is on its prototype chain and does not exist in unexpectedKeyCache (unexpectedKeyCache value is {})
        key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
    )
    //Add unexpected key s in state to unexpectedKeyCache, and set the value to true    
    unexpectedKeys.forEach(key => {
        unexpectedKeyCache[key] = true
    })
    //If action.type belongs to the replacement type, that is, when replacing reducer, return directly without the following checks
    if (action && action.type === ActionTypes.REPLACE) return
    //Determine whether unexpectedKeys is longer than 0 and return an incorrect Prompt string if greater than 0.
    //The function here is, if you initialize an attribute on a state that is not an attribute of its own, but on its prototype chain.
    //If you change state, this property will be ignored. What does that mean? For example, if your initialization state = age: 23},
    //The state prototype object has an attribute name:'Zhang San', which you can access in the application, but if you do
    //Once dispatch, the name attribute is lost, because the redux update state is the entire replacement.
    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.`
        )
    }
}

Here's the main code for combineReducers

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

First, a Boolean variable is declared to record whether the state has changed and the initial value is false. Then declare a nextState variable to store the next state and initialize it as an empty object.

Then the data of reducer object keys are stored in a loop, and each key name key is obtained. If it is not renamed, the key is the function name of the reducer function. Then the corresponding reducer is retrieved from the object where the reducer is stored by the key, and the last state value corresponding to each reducer is obtained and stored in the variable previousStateForKey. Then generate the next state through the current reducer

const nextStateForKey = reducer(previousStateForKey, action)

The state passed in from reducer is the last state, so we can get the last state in reducer.

Next, determine whether the value of nextStateForKey is undefined, and if so, throw an error, which means that we never return undefined in reducer.

The getUndefinedStateErrorMessage function is used to generate error messages. The source code is as follows:

function getUndefinedStateErrorMessage(key, action) {
    //When action exists, assign the value of action.type to the actionType variable
    const actionType = action && action.type
    //Description of action
    const actionDescription =
        (actionType && `action "${String(actionType)}"`) || 'an action'
    //Returns the error message for the prompt
    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.`
    )
}
nextState[key] = nextStateForKey

Save the generated state in the root state according to the key of reducer, which is a little bit around here. I'll give you an example later.

Next up is a judgment to see if there has been a change in reducer's state.

hasChanged = hasChanged || nextStateForKey !== previousStateForKey

The initial value of hasChanged is false, so for the first time, the expression must go back. The latter expression is to judge whether the last state is equal to the next state. When the first cycle is over, the value of hasChanged is true, so the next cycle will not go back.

Finally, the state is returned according to hasChanged, and if there is no change, the original state is returned. Return to nextState if there is a change.

The following examples illustrate:

function user (state={},action) {
    switch (action.type) {
        //...
        default:
           return state
    }
}

function menus(state{},action) {
    switch (action.type) {
        //...
        default:
           return state
    }
}

const reducers = combineReducers({user,menus})

The reducers function we get here is the combination function returned by combineReducers. It is also a reducer function. The combination forms a closure in which we store our own defined reducer in the form of objects. As follows:

finalReducers = {
    user: function user(state,action){...},
    menus: function menus(state,action) {...}
}

When we call the createStore function to pass in the combination function, it initializes the execution of the combination function once, and the value returned by the combination is assigned to the currentState of the createStore function. According to the execution of the combination function, we know that its return value should be an object. The key of the object is the name of the corresponding reducer function, which is the following value.

nextState = {
    user:{},
    menus:{}
}

The state managed by the combined reducer is placed in the same object with the key as the reducer name (which you can define yourself, of course), so that we can divide the reducer iteration into infinite layers.

In this example, the final current state is nextState. You can also merge the merged reducer with other reducers. CurrtState is the root of a tree. There are many branches below, and there are smaller branches on the branches. So we also call it store tree.

 

Posted by jhlove on Wed, 24 Jul 2019 22:10:49 -0700