I like this sentence very much: Master the truth and you will be free. Indeed, in the code world, knowing the truth allows you to do whatever you want.

Redux is small (less than 7K), but has a rich (several hundred) plugin ecosystem. How extensible it is to have so many plug-ins. Each line of redux and its middleware source code (version v4.0.5) will not only tell you what it means, but also why it is written. .

1 introduction

What is a redux? To quote the official word: Redux is a predictable state management application designed for JS. It helps you write applications that behave consistently, run in different environments (client, server, native), and are very easy to test. In addition, it provides a great developer experience, such as real-time code editing combined with a time-travel debugger. You can use react or any other visual library.

To put it bluntly, Redux is the state management library -Store, which helps us store some state – states and provides a method -Dispatch to modify state states. At the same time, we can subscribe to changes in the -subscribe state and send notifications after each state change (dispatch call) to process the logic.

Basic nouns

  • store: is a container that contains the state (state), the method of modifying the state (dispatch()) and subscribe(), etc.
  • state: Holds state-related data, which can only be normal objects, arrays, or raw values, usually normal objects.
  • action: Action behavior, which is a common object that must have the type attribute to describe the current behavior.
  • dispatch(): Dispatcher, dispensing an action (action), which is the only modified state (state).
  • reducer(): Indicates the processorstateandaction. throughactionTo return a new onestate.
  • subscribe()Subscriber that receives one callback function at a timestateChange, call this function.

Expand the noun

  • actionType: refers to the type of action.
  • actionCreator(): generates the action function.
  • Pure functions: 1. The same input must have the same output. 2. There will be no side effects, that is, no change in the outside state.It is the guarantee of redux’s predictable functionality.

Three core

  • Single data source: Indicates that the global state of the application should be stored in a single store (the application has only one store).
  • stateRead only:stateYes cannot be changed directly, the only way to change it is to call itdispatch()Methods.
  • Returns a new state with a pure function: the pure function refers toreducer().

A single data source facilitates debugging and detection; Read-only state can prevent any other Modal layer or View layer from directly modifying state, resulting in difficult to reproduce the bug, so as to ensure that all internal state changes are centrally controlled, and strictly in accordance with the order; Using pure function to return the new state, we can be very convenient to record the user’s operation, to achieve undo, redo, time travel and other functions.

Note:

  1. Modify thestateOnly throughdispatchMethod to avoid direct modificationstateThe value of the. If YOU change it directlystateThe value of a property may change, but the change is not detected by the subscriber, causing bugs that are hard to reproduce.
  2. Forcing actions to describe each change makes it clear what is happening in the application. If a state changes, we can also know why it changed.

2 createStore()

With this function, we can create a store.

CreateStore () can be passed three arguments:

  • reducer(mandatory) is a function, depending on theactionTo return a new onestate.
  • preloadedState(Optional) Initializestate
  • enhancer: (Optional) is an enhancement function, usually used with middleware. Can beApplyMiddleware functions

Usage:

< reducer > const store = createStore(reducer, preloadedState, enhancer) Or, if the initial value is not set in this function (instead, it is set in the reducer function), it can be used directlyCopy the code

Reducer () can receive two parameters:

  • state: (required) is the initial value
  • action: (mandatory) is an object containingtype(Trigger behavior type) properties, and data-related properties.

Usage:

const initState = { num: 1, } const reducer = (state = initState, action) { const num = state.num switch (action.type) { case "add": return {... state, num: num + 1}; case "minus": return {... state, num: num - 1}; default: return state } }Copy the code

The above store will return four methods: store.getstate (), store.dispatch(), store.subscribe(), store.replacereducer () and so on.

The source code is as follows:

Function createStore(reducer, preloadedState, enhancer) {// If the second and third arguments are both functions, or the third and fourth arguments are both functions, an error will be raised. if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { throw new Error( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them '+ 'together to a single function. If the third parameter is undefined, then the call method is createStore(Reducer, enhancer), and preloadedState is enhancer. // Set enhancer to preloadedState. If (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {enhancer = PreloadedState preloadedState undefined} / * * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = the following is the main content = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * * / / / if the participation in the form of a similar createStore (reducer, preloadedState. {}) // Because enhancer can only be a function, an error is reported if the enhancer is not a function. if (typeof enhancer ! == 'undefined') { if (typeof enhancer ! == 'function') {throw new Error('Expected the enhancer to be a function.') Enhancer (createStore)(Reducer, preloadedState)} == 'function') {throw new Error('Expected the reducer to be a function.')} // Save the incoming reducer let in currentReducer CurrentReducer = Reducer // The reducer will pass in initialization state(preloadedState), CurrentListeners = preloadedState [] CurrentListeners = // Check whether the dispatch method is being executed getState(){... } function subscribe(listener) {... } function dispatch(action) {... } function replaceReducer(nextReducer) {... } // Execute the dispatch method once, in order to get the initial state from each reducer. dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer } }Copy the code

Let’s first introduce the implementation of each method separately

2.1 getState ()

With this method, we can get the latest state.

Usage:

    const newState = store.getState()
Copy the code

The getState() method in the source code is very simple. It directly returns the internal variable currentState, whose value is state.

/ function getState() {if (isDispatching) {// An error is raised if the dispatch method is being executed. throw new Error( 'You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.' ) } return currentState }Copy the code

The source code also makes a judgment that if the dispatch process is currently in progress, an error will be thrown.

2.2 dispatch ()

This method is the only way we can change state. It can pass in an action with a type attribute that describes the current behavior.

Usage:

    const addAction = {type: "add"}
    store.dispatch(addAction)
Copy the code

The source code is as follows:

Function dispatch(action) {// If it is not an object, throw an error if (! isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' If (typeof action.type === 'undefined') {throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant? ')} // To avoid calling dispatch() in the reducer function, which would cause an infinite loop. So an error is thrown. if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } / * * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = the following is the main content =======================================================**/ try { isDispatching = true // CurrentReducer is a reducer passed in by createStore that calls the reducer function and then assigns values to internal variables to save. CurrentState = currentReducer(currentState, Action)} finally {isDispatching = false} const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; Listener = listeners[I] listener()}Copy the code

Thought 1: Why does the above code use try… Finally, why not use try… The catch?

The reason is that there may be an error when the user enters the reducer function. Because catch is not used, this error will be thrown and the function will terminate. This is not hard to understand.

What about finally? In fact, isDispatching is reset whether there are errors or not. If there are errors, it prevents all dispatching () from failing.

Thought 2: If the dispatch() method is called multiple times at the same time, then all subscribed functions will be called multiple times? Or why doesn’t Dispatch () accept an array so it can dispatch multiple acitons?

If the dispatch() method is called multiple times at the same time, it’s a sure bet that all subscribed functions will be called multiple times.

Why dispatch() doesn’t take an array as an argument. If this happens, the first thing to consider is, have you defined the action properly? There is no need to use multiple actions when one action is perfectly capable of solving a problem. However, if you want to trigger multiple actions at the same time, but only once, you can use high order Reducer to solve the problem. See:

The community also has plugins

Consider 3: Dispatch () why there is no judgment: if state changes, all subscribed functions will be called. Wouldn’t that improve performance?

Yes, it improves performance, but Redux puts control in the hands of the user, who defines it. If you want a scenario where subscribers are notified every time after dispatch(), regardless of whether the state has changed. Then thank the author for leaving you in control.

Summary of main functions:

  • callreducer()Function returns a newstate
  • Call all subscribed functions, executed in sequence.
  • You can also see this through the following code:reducer()Cannot be used ingetState().dispatch().subscribe()Methods. If used, an error is reported.
Try {isDispatching = true // Call the user to pass the reducer function, and then assign the values to internal variables to save. currentState = currentReducer(currentState, action) } finally { isDispatching = false }Copy the code

2.3 the subscribe ()

It can subscribe to changes in state, and it receives a callback function that is added to an internal queue to be stored, and when the state changes, the callback function is executed.

It executes the result and returns a function after which the previously passed callback function is removed from the internal queue to unsubscribe.

Usage:

// subscribe const unsubscribe = store.subscribe(function stateChange() {console.log("newState:", Store.getstate ())}) // Unsubscribe ()Copy the code

The source code is as follows:

Function subscribe(listener) {// If it is not a function, throw an error if (typeof listener! == 'function') {throw new Error('Expected the listener to be a function.')} If (isDispatching) {throw new Error('You may not call store. Subscribe () while the reducer is executing. '+ 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribelistener for more details.' ) } / * * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = the following is the main content = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * * / / / a sign of whether to subscribe to the let isSubscribed = true / / main functions: If nextListeners are equal to currentListeners, Is currentListeners copy the results of the assigned to nextListeners ensureCanMutateNextListeners () / / add subscription function to the queue nextListeners. Push (the listener) / / Return function unsubscribe() {// If it has been removed from the queue, it simply returns. Prevents users from unsubscribing multiple times. if (! IsSubscribed) {return} // During the 'dispatch' process, If (isDispatching) {throw new Error('You may not unsubscribe from a store listener while the reducer is '+ 'See https://redux.js.org/api-reference/store#subscribelistener for more details.')} // Set the unsubscribe flag to false IsSubscribed = false If nextListeners are equal to currentListeners, Is currentListeners copy the results of the assigned to nextListeners ensureCanMutateNextListeners () / / remove the subscription function const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) currentListeners = null } }Copy the code

The above ensureCanMutateNextListeners () as follows:

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }
Copy the code

Reflection 1: Why are nextListeners needed and do they serve any purpose? Can’t just the currentListeners?

We know nextListeners and currentListeners are used to store subscription function, from the function ensureCanMutateNextListeners () can also be, The purpose of nextListeners is to duplicate the currentListeners. Is there any problem with not copying, or just using currentListeners?

Note that the listeners may continue to subscribe or unsubscribe while the callbacks are running.

Const unsubscribe = store. Subscribe (function A() {// Unsubscribe ()})Copy the code

Remember the following code from the dispatch() function:

// Retrieve all the saved subscription functions, and execute. const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() }Copy the code

When the subscriber’s callbacks are executed sequentially, such as A being first in the queue (index 0), and B,C,D, and so on.

If A has unsubscribed, that is, removed itself from the queue, then the gallipoll.length has changed and the gallipoll.length is 0. The next loop, with an index of 1, will result in B not executing. If A continues to subscribe after execution, the listeners’ length will change, and the last callback in the queue will be the new callback, which will be triggered immediately in this cycle, but this is not in accordance with redux’s design concept. The new subscription function should be triggered at the next dispatch().

Thinking 2: Then thinking 1, one might say, then why did we change dispatch() to the following form:

    const listeners = currentListeners.slice()
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
Copy the code

It’s true that changing to this format is a solution to this problem, and I thought so until I found the redux submission record: You can see that the original Redux code was written in a similar way. It doesn’t matter.

The submission message says something like: If we write this, wouldn’t we copy a queue every time we dispatch? This is a waste of resources and should be copied when needed, when subscribed or unsubscribed.

CurrentListeners = nextListeners can ensure that they are the same on each dispatch. , will certainly be executed ensureCanMutateNextListeners () function to replicate.

Copying on subscription or unsubscription has the additional benefit of ensuring that the current subscription or unsubscription will take effect at the next dispatch().

Finally, ensureCanMutateNextListeners why still need to decide if () function (nextListeners = = = currentListeners), a copy of directly? This is also to avoid unnecessary duplication when subscription callbacks are added multiple times at the same time.

Thought 3: Why are currentListeners = null at the end of an unsubscribe function?

The reason for this is that the callback listeners are only deleted from the nextListeners when we unsubscribe, but the currentListeners are still storing the cancelled callback functions, causing a memory leak. CurrentListeners are not used anywhere else and can be set to null.

Thought 4: Why doesn’t SUBSCRIBE () pass state as an argument to the callback instead of using store.getState() each time?

Subscribe (() => console.log(store.getState())); Subscribe (state => console.log(state)); // Instead of passing 'state' directly as an argument, store. Subscribe (state => console.log(state));Copy the code

I also feel it is very convenient. This question is also one of the issues that developers have raised for Redux. I searched for the issue and finally found the author’s answer.

To summarize what the author means:

If you pass state as an argument, shouldn’t you also pass previousState so that developers can easily compare and process different logic? But this is to make sure that the store works when it is detected by Redux DevTools. The authors argue that every store extension that uses the getState() method also has to pay special attention to this parameter, which feels like the price you pay to make low-level apis more consumer-friendly.

The Store API is extensible, which requires that the functionality of each function be as simple and atomized as possible, rather than repeated. If an extension wants to do something before state is passed to the consumer, and if we pass state, then we have to deal with it in two places, it’s easy to get wrong.

Think 5: Subscribe () What happens if you subscribe to the same function more than once?

For example, the nextListeners contain two identical functions subscribed to by component A and B, but the subscribe() function does not check, so unsubscribes on component B are actually A callback that unsubscribes on component A.

Redux officially explains that this is a rare case and there is no actual usage scenario. So we want to avoid this in real development.

Summary of main functions:

  • It adds the subscribed callback function to the queue for saving.
  • subscribe()The return value of is a method that can be called to unsubscribe.
  • insubscribe(function() {store.dispatch()})It will cause an infinite loop.

2.4 replaceReducer ()

It can be used to replace the current reducer() function. This is a high-level REDUCER API that you can use if you want to dynamically load the reducer.

Usage:

    store.replaceReducer(newReducer)
Copy the code

The source code is as follows:

Function replaceReducer(nextReducer) {// Throw an error if it is not a function. if (typeof nextReducer ! == 'function') {throw new Error('Expected the nextReducer to be a function.')} currentReducer = NextReducer // Triggers an action so that store.getState() gets the latest value. dispatch({ type: ActionTypes.REPLACE }) }Copy the code

3 combineReducers()

As the application becomes more and more complex, you may split the Reducer () function into multiple reducer(), and each reducer() has its own state. A Reducer () cannot meet the requirements, then combineReducers() came into being.

  • It receives an object with a value for each keyreducer()Function.
  • Its return value is mergedreducer()Function. It’s computed from this return valuestateIt’s also an object,keyWith the incomingcombineRedeucers()thekeyIf yes, value is correspondingstate.

Usage:

    import {reducer1, reducer2, reducer3} = "./reducer"
    const reducer = combineReducers({reducer1, reducer2, reducer3})
    const store = createStore(reducer)
Copy the code

The main source of the function is actually not much, most of it is to determine whether the value passed in by the user is correct.

  1. As follows:
Function combineReducers(reducers) {// Get the passed key const reducerKeys = object.keys (reducers) // The processed reducers const FinalReducers = {} // loop, judge that the reducer type passed in is undefined, then warning; if the reducer type is function, add it to 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 = Keys (finalReducers) // Cache initialization has keys in the passed state, but not in finalReducers. let unexpectedKeyCache if (process.env.NODE_ENV ! == 'production') {unexpectedKeyCache = {}} let shapeAssertionError try {// To check whether each reducer carries the default value, And whether the built-in actionType is used in the Reducer. AssertReducerShape (finalReducers)} Catch (e) {// If the default value is not set or the internal 'actionType' of redux is used, it is cached first. ShapeAssertionError = e} / * * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = the following is the main content = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * * / / / final reducer function return function combination (state = {}, Action) {// If no default value is set for each reducer, or the reducer function is the reducer function if built-in actionType is used, an error is raised. If (shapeAssertionError) {throw shapeAssertionError} // Initialize a key that is present in the passed state but not in finalReducers. Throws a warning. if (process.env.NODE_ENV ! == 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, UnexpectedKeyCache) if (warningMessage) {warning(warningMessage)}} // Check whether state changes let hasChanged = false // final state  const nextState = {} for (let i = 0; i < finalReducerKeys.length; I ++) {const key = finalReducerKeys[I] // Current key const reducer = finalReducers[key] // Current reducer const PreviousStateForKey = state[key] // Old state const nextStateForKey = Reducer (previousStateForKey, Action) // New state // If the new state is undefined, an error is thrown. // The reducer return value has already been validated. // It is possible that the user returns undefined for an actionType. The main reason is to avoid bugs. if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, Action) throw new Error(errorMessage)} nextState[key] = nextStateForKey If nextStateForKey and previousStateForKey are not equal, hasChanged is true and the next loop does not need to continue comparing their values. hasChanged = hasChanged || nextStateForKey ! == previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length ! == Object.keys(state).length return hasChanged ? nextState : state } }Copy the code

As you can see from the above code, hasChanged and are true only if

  • finalReducersAny one of thereducersReturns true if the value is different from the last time.
  • finalReducerKeysIs true if the length of the key is different from the length of the key passed in the initialized state.

Consider 1 why shapeAssertionError is not thrown directly, but in the returned function.

If thrown directly, the entire JS thread will abort, creatStore() will fail, and the React application will fail to build. The author doesn’t think it’s appropriate to throw an error here. Instead, it is thrown directly at dispatch() every time it is called, so combination() is the friendliest error.

Think 2: Why judge finalreducerkeys.length! == object.keys (state).length

Although there is an unexpected check for the initialization key(that is, in the unexpectedKeyCache variable), it only throws a warning, not an error, if there is an unexpectedKeyCache (that is, the above equation does not hold) hasChanged is also considered true. (The moment when this equation fails is only at the first dispatch(), since the subsequent states are calculated according to finalReducers, the two cannot be equal.)

  1. The following isassertReducerShape()Function code:
Function assertReducerShape(reducers) {// Pass finalReducers and check whether the reducer result is undefined, Keys (reducers). ForEach (key => {const reducer = reducers[key] // Use internal ActionTypes.INIT to test the return value of the reducer const initialState = Reducer (undefined, {type: Actiontypes.init}) // If the result is undefined, the reducer does not return the default value. 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, // ActionTypes.PROBE_UNKNOWN_ACTION() will return a random type. // There are two ways to enter this step: // 1, the current Reducer has judged that type is actiontypes. INIT and returned a non-undefined value. ActionTypes.INIT is not determined in the current Reducer, that is, if the Reducer does not recognize an actionType, it will return a non-undefined value (which is what we expected). If the reducer returns undefined, the second case above can be excluded, indicating that the code uses type only for internal use of REdux. In this case, an error is thrown. 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

Thought 1: What if we passed an initial value in createStore(), but the separate Reducer () function did not set the initial value?

Here’s an example:

import {reducer1, reducer2, reducer3} = "./reducer" const reducer = combineReducers({reducer1, reducer2, reducer3}) const initState = {reducer1: 1, reducer2: 2, reducer3: Const store = createStore(reducer, initState)Copy the code

The reducer1 function has the following form, that is, no default values are set in the function:

    reducer1(state, action) {
       return state 
    }
Copy the code

There is a line of code in the assertReducerShape() function above: const initialState = reducer(undefined, {type: Actiontypes.init}), this code does not pass the createStore() initial value because undefined.

Therefore, it is best to set the initial values in each independent reducer() function.

Thought 2: Then thought 1, why does each independent reducer() of assertReducerShape() pass in undefined?

Because the reducer() function returns undefined, the robustness of the reducer() function written by the user can be better judged by passing undefined.

Thought 3: After each dispatch(), all reducer() will be executed. Will this affect performance?

According to the official opinion, although it will affect, it can be ignored, because the JS engine can run a large number of functions per second, and the reducer function is pure function, so it can hold all the reducer functions. If you’re really worried about performance, you can use plug-ins.

4 bindActionCreators()

ActionCreator () is an object in the form of {type: “ADD”, value: 2}. ActionCreator (), as its name implies, is the function that generates the action, as follows:

// getAddAction is an actionCreator() function const getAddAction = (value) => {type: "ADD", value}Copy the code

Without further explanation, bindActionCreators() is a combination of the actionCreator() and dispatch() functions. It takes two arguments, the first function/object and the second dispatch.

  • If the first argument is a function (some actionCreator()), return a function (actionCreator() and dispatch() integration function)

  • If the first argument is an object (multiple ActionCreators ()), an object containing the same key is returned. This is used for mapDispatchToProps in React-redux.

Usage:

const actionCreators = { addAction(value) { return {type: "ADD", value} }, delAction(value) { return { type: 'DEL', value } } } const boundActions = bindActionCreators(action, dispatch) boundActions.addAction(2); // Equivalent to dispatch(actionactions.addAction (2)) boundactions.delaction (1); // Equivalent to dispatch(actionactions.delaction (1))Copy the code

The source code for bindActionCreators() is as follows:

Function bindActionCreators(actionCreators, dispatch) {// If is a function, Call the bindActionCreator function if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, } // If actionCreators is not an object and is not null, an error is thrown if (Typeof actionCreators! == 'object' || actionCreators === null) { throw new Error( `bindActionCreators expected an object or a function, instead received ${ actionCreators === null ? 'null' : typeof actionCreators }. ` + `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"? ')} // is the object, then loop, Call the bindActionCreator function const boundActionCreators = {} for (const key in actionCreators) {const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators }Copy the code

BindActionCreator ()

    function bindActionCreator(actionCreator, dispatch) {
      return function() {
        return dispatch(actionCreator.apply(this, arguments))
      }
    }
Copy the code

5 compose()

Compose (a, B,c) : compose(a, B,c) : compose(a, B,c) : compose(a, B,c)) Args) => a(b(c(args))). Typically used to combine multiple middleware functions.

The source code is as follows:

export default function compose(... Funcs) {return arg => arg if (funcs.length === 0) {return arg => arg} If (funcs.length === 1) {return funcs[0]} // Return funcs.reduce((a, b) => (... args) => a(b(... args))) }Copy the code

6 applyMiddleware()

ApplyMiddleware () receives multiple middleware functions and is used as follows:

    const store = createStore(reducer, applyMiddleware(middlewareFn))
Copy the code

6.1 Origin of Middleware Functions

Website example

  1. If after every state change, it’s going to beactionAnd the newstateTo record, we usually do something like this:
    const action = addTodo('Use Redux')

    console.log('dispatching', action)
    store.dispatch(action)
    console.log('next state', store.getState())
Copy the code
  1. The above approach can be implemented, but we don’t want to write too much code at a time. It might be encapsulated as follows:
function dispatchAndLog(store, action) { console.log('dispatching', Action) store.dispatch(action) console.log('next state', store.getstate ())} addTodo('Use Redux'))Copy the code
  1. At this point, the problem is solved, but each time the function (dispatchAndLog()) and calldispatchAndLog(store, addTodo('Use Redux'))In this case, we may continue to do the following encapsulation:
    const next = store.dispatch
    store.dispatch = function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
Copy the code

By reassigning dispatch, we can do this for good. But then the question arises, what if I have to apply this transformation multiple times? For example: Every time I dispatch(), I need to log an error. What should I do?

  1. What if we were to implement an error-reporting middleware? (One might say, record error, can be usedwindow.onerror, but it is not reliable because in some older browsers it does not provide stack information (which is crucial to understanding why the error occurred). If the calldispatch()Wouldn’t it be nice if any errors were reported?

We know that it is important to separate record-keeping and error reporting from each other, and that they should belong to different modules, so we might encapsulate them as follows:

function patchStoreToAddLogging(store) { const next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } function patchStoreToAddCrashReporting(store) { const next = store.dispatch store.dispatch = function dispatchAndReportErrors(action) { try { return next(action) } catch (err) { console.error('Caught an exception! PostData ("/error", {error, action, state: postData("/error", {error, action, state: Store. GetState ()}) throw err}}} / / call patchStoreToAddLogging (store) patchStoreToAddCrashReporting (store)Copy the code
  1. But the above ones are still not very good, intrusive, and fall under the category of MONKEY PATCHING. It is hoped that each function function will not modify the external variables or functions as much as possible, resulting in some side effects. So if we take this function back and we take it outside. For example, wouldn’t it be nice if we changed the above function to the following form:
Function Logger (store) {const next = store. Dispatch: // store.dispatch = function dispatchAndLog(action) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }Copy the code

And there’s also an “assistant” inside Redux to help us deal with this:

Function applyMiddlewareByMonkeypatching (store, middlewares) {/ /, is the purpose of this step in the previous code, we applied the two middleware, Then when we dispatch an action, the last middleware executes first, which is obviously not what most people are used to. Middlewares.reverse () middlewares.foreach (middleware => (store.dispatch = middleware(store)))}  applyMiddlewareByMonkeypatching(store, [logger, crashReporter])Copy the code
  1. The logger function above, however, is still MONKEY PATCHING. .

There’s another way to implement Logger. What if instead of next = store.dispatch, we implement logger as an argument?

    function logger(store) {
      return function wrapDispatchToAddLogging(next) {
        return function dispatchAndLog(action) {
          console.log('dispatching', action)
          let result = next(action)
          console.log('next state', store.getState())
          return result
        }
      }
    }
Copy the code

So at this point, we will applyMiddlewareByMonkeypatching renamed applyMiddleware, and put it into the following:

    function applyMiddleware(store, middlewares) {
      middlewares.reverse()
      let dispatch = store.dispatch
      middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
      return Object.assign({}, store, { dispatch })
    }
Copy the code

As a result, the applyMiddleware function looks similar to the source code, but differs in three other ways:

  • There is no exposure in the source codestoreThe entire API is going to bedispatch()andgetState()Methods.
  • Source code made a judgment, if in the building of middleware, calldispatch()Method returns an error.
  • To ensure that the middleware can only be applied once, the time to apply the middleware is increateStore()Execution time.

The source code is as follows:

function applyMiddleware(... middlewares) { return createStore => (... args) => { const store = createStore(... Let dispatch = () => {throw new Error('Dispatching while constructing your middleware is not allowed. '  + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch) return { ... store, dispatch } } }Copy the code

Remember enhancer(Reducer)(Reducer, preloadedState) from createStore(Reducer, preloadedState)? This enhancer parameter is usually applyMiddleware().

Thought 1: Why is the source code written like this: (… args) => dispatch(… Args) instead of dispatch: dispatch?

The middlewareapi. dispatch function returns a new dispatch. If dispatch: dispatch is written, middlewareapi. dispatch will always be the following function:

   let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
Copy the code

Chestnut:

Let fn () = = > console. The log (" a ") let obj = {fn} fn = () = > console. The log (" b "). The console log (obj. Fn) / / for (a) = > console.log("a")Copy the code

Thought 2: So the question comes up again, and then thought 1, why do you set the initial value of dispatch to a function that throws an error?

This is to prevent the dispatch() method from being called immediately when the middleware is built (that is, layer 1 and layer 2 inside the middleware).

For example, the following middleware:

({dispatch, getState}) => {an error is reported if dispatch() is called immediately. dispatch() return next => action }Copy the code

Thinking about 3: Middlewares. ForEach (middleware => (dispatch = middleware(store)(dispatch))). The actual source code is assigned only once. How is this done?

This is done using the compose() method, which converts the form (compose(a, b, c)) to (… Args) => a(b(c(args))). This return is passed to the Dispatch to wrap the Dispatch () layer by layer.

Such as: (… Args) => A (b(c(args))).

  • Args of c, args of cnext()) is primitivedispatch()The return value is the new value returned by the C middlewaredispatch().
  • Similarly: for parameters of function B (thennext()Is cdispatch().
  • For the parameters of function Anext()) is bdispatch()

So when we finally call the store.dispatch() method, the final execution order of each middleware is:

  • A,dispatch()Start executing.
  • b.dispatch()Start executing.
  • cdipatch()Start executing.
  • cdispatch()No further action is required.
  • b.dispatch()No further action is required.
  • A,dispatch()No further action is required.

The order of execution is the Onion:

Therefore, after invoking Dispatch at the third layer of middleware, all middleware is executed. Calling next invokes the next middleware.

7 story – thunk the source code

The thunk middleware function is not hard to understand, except with the extraArgument function.

function createThunkMiddleware(extraArgument) { return ({ dispatch, GetState}) => (next) => (action) => {// Pass dispatch if it is a function. if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;Copy the code

Method of use

const store = createStore(reducer, applyMiddleware(thunk)) const asyncAction = async (dispatch) => { let res = await postData('/a') dispatch(action) } // At this point you can dispatch a function called store.dispatch(asyncAction)Copy the code