preface

Read by: Developers who have worked with Redux and don’t really understand how redux works.

During my internship training, my training brother told me that the core source code of Redux was very simple and suggested me to have a look at it when I was free to improve my understanding of the Redux series.

I have been involved in many projects of the company for more than a month since I joined the company. I have also used Redux for some time, but I still don’t have a deep understanding of Redux. I still stay at the stage of “knowing how to use it, but not the core principle”.

So I pulled the source code of Redux on Github, looked at it for a while, and found that there are not many things, relatively simple.

What is the function of Redux itself

In projects, redux is not used purely. Instead, we use other libraries to improve efficiency, such as React-Redux, which makes it easier for React applications to use Redux, and wepy-Redux, which provides a library for the applets framework wepy.

But in this article, the scope is more pure, just redux itself.

What does Redux itself do? Let’s take a quick look at the core idea (workflow) of Redux:

  • The state is consolidated into a single state, which is managed by the Store.
  • This store was created according to the Reducer “shape”.
  • Reducer is a reducer that outputs a new state after receiving an action and accordingly updates the state on the store.
  • According to redux principle guidance, the best way to externally change state is to call store dispatch method and trigger an action, which is processed by the corresponding Reducer to complete state update.
  • You can add a listener function to the store via subscribe. Every time the Dispatch method is called, all the listener functions are executed.
  • Middleware can be added (we’ll talk about what middleware does later) to handle side effects.

In this workflow, the functions redux needs to provide are:

  • Create a store, that is:createStore()
  • The created store providessubscribe.dispatch.getStateThese methods.
  • Merge multiple reducers into one Reducer, that is:combineReducers()
  • Application middleware, i.eapplyMiddleware()

That’s right, so let’s take a look at the source directory for Redux:

Do so much, as to compose, bindActionCreators, is some tools.

We’ll take a look at createStore, combineReducers, applyMiddleware, and compose one by one.

It is recommended to open the link: redux source code address, refer to this article to read the source code.

The realization of the createStore

The general structure of this function looks like this:

function createStore(reducer, preloadedState, enhancer) {
    ifEnhancer is valid {// We will explain this later, so we can ignore it for now
        return enhancer(createStore)(reducer, preloadedState)
    } 
    
    let currentReducer = reducer Reducer in the current store
    let currentState = preloadedState // The state of the current store
    let currentListeners = [] // The listener placed in the current store
    let nextListeners = currentListeners // The next dispatch listener
    // Note: when we add a listener, it only takes effect at the next dispatch.
    
    / /...
    
    / / for the state
    function getState() {
        / /...
    }
    
    // Add a listener that will be executed whenever dispatch is called
    function subscribe() {
        / /...
    }
    
    // An action was triggered, so we called the reducer, got the new state, and executed all the listening functions added to the store.
    function dispatch() {
        / /...
    }
   
    / /...
    
    //dispatch a reducer action to initialize
    Then obtain the initial reducer value from the reducer as well
    // See the reducer implementation below.
    
    
    return {
        dispatch,
        subscribe,
        getState,
        // The following two methods are mainly for library developers, which are ignored for the moment
        //replaceReducer,
        //observable}}Copy the code

As you can see, the createStore method creates a store, but instead of returning the store’s state directly, it returns a set of methods that can be used externally to getState. Or change state indirectly (by calling Dispatch).

And state, it’s stored in a closure. (For those of you who don’t understand closures, go ahead and check them out.)

Let’s take a closer look at how each module is implemented (error-handling code has been omitted for clarity) :

getState
function getState() {
    return currentState
}
Copy the code

It’s so simple. This is much like the way in object-oriented programming that encapsulates read-only properties and provides getters for data rather than setters directly. (Although the return is a reference to state, you can modify state directly, but in general, Redux does not recommend doing so.)

subscribe
function subscribe(listener) {
    // Add to the listener array,
    // Note: we added an array that won't take effect until the next dispatch
    nextListeners.push(listener)
    
    let isSubscribe = true // Sets a flag indicating that the listener is subscribed
    // Return the unsubscribe function, that is, remove the listener from the array
    return function unsubscribe() {
        if(! isSubscribe) {return // If the subscription has already been unsubscribed, return directly
        }
        
        isSubscribe = false
        // Remove this listener from the next round's listener array (for the next dispatch).
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)}}Copy the code

Subscribe returns a method to unsubscribe. Unsubscribing is essential, and when added listeners become unusable, they should be removed from the store. Otherwise this useless listener would be called every time dispatch.

dispatch
function dispatch(action) {
    // Call reducer to get the new state
    currentState = currentReducer(currentState, action);
    
    // Update the listener array
    currentListener = nextListener;
    // Call all the listener functions in the listener array
    for(let i = 0; i < currentListener.length; i++) {
        constlistener = currentListener[i]; listener(); }}Copy the code

The basic functions of the createStore method have been achieved, but calling the createStore method needs to provide reducer. Let’s think about the role of reducer.

combineReducers

Before we understand combineReducers, let’s think about the reducer function: Reducer accepts an old state and an action, and when this action is triggered, reducer returns a new state.

That is, the Reducer is responsible for state management (or updating). In practice, the status of our application can be divided into many modules. For example, the status of a typical social networking site can be divided into user personal information, friend list, message list and other modules. Theoretically, we could use a reducer to handle all state maintenance, but if we did, we would have too much logic in a reducer function, which would easily lead to confusion.

Therefore, we can also divide logic (reducer) according to modules, and each module is subdivided into sub-modules. After developing the logic of each module, merge the reducer, so that our logic can be clearly combined.

For our needs, Redux provides combineReducers, which merges the reducer into a total reducer.

Redux combineReducers

function combineReducers(reducers) {
    // Get all the keys passed into the Reducers object first
    const reducerKeys = Object.keys(reducers)
    const finalReducers = {} // Finally, the real reducer exists here
    
    // Select effective reducer from reducers below
    for(let i = 0; i < reducerKeys.length; i++){
        const key  = reducerKeys[i]
        
        if(typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key] 
        }
    }
    const finalReducerKeys = Object.keys(finalReducers);
    
    // The assertReducerShape function does this:
    Check whether the Reducer in finalReducer can still return a valid value when it accepts an initial action or an unknown action.
    let shapeAssertionError
  	try {
    	assertReducerShape(finalReducers)
  	} catch (e) {
    	shapeAssertionError = e
  	}
    
    // Return to the reducer after the merge
    return function combination(state= {}, action){
  		// The logic here is:
    	// Obtain the state corresponding to each reducer, and use it as a parameter with the actions to execute on each reducer.
    	let hasChanged = false // Flag whether state has changed
        let nextState = {}
        for(let i = 0; i < finalReducerKeys.length; i++) {
                    // Get the reducer of this loop
            const key = finalReducerKeys[i]
            const reducer = finalReducers[key]
            // Get the old state corresponding to this reducer
            const previousStateForKey = state[key]
            Call reducer to get the new state
            const nextStateForKey = reducer(previousStateForKey, action)
            // Save to nextState
            nextState[key] = nextStateForKey
            // Here is a problem:
            If the reducer cannot handle the action, previousStateForKey is returned
            // This is the old state. When all the states have not changed, we just return to the old state.hasChanged = hasChanged || previousStateForKey ! == nextStateForKey }return hasChanged ? nextState : state
    }
} 
Copy the code

Why middleware is needed

Reducer should be a pure function in the design of Redux

Wikipedia definition of pure functions:

In programming, a function may be considered pure if it meets the following requirements:

  • This function needs to produce the same output at the same input value. The output of a function is independent of any hidden information or state other than the input value, or of the external output generated by the I/O device.
  • This function cannot have semantically observable function side effects, such as “firing events,” making output devices output, or changing the contents of objects other than output values.

The output of a pure function may not depend on all input values, or even be independent of all input values. But the output of a pure function cannot be related to any information other than the input value. A pure function can return multiple outputs, but the above principle must be true for all outputs. If arguments are called by reference, changes to the argument object will affect the contents of objects outside the function and therefore are not pure functions.

To sum up, the point of pure functions is:

  • The same input produces the same output (cannot be used internallyMath.random.Date.nowThese methods affect the output.
  • The output cannot be related to anything other than the input value (no API can be called to get additional data)
  • The inside of the function cannot affect anything outside the function (cannot directly change the reference variable passed in), that is, cannot mutate

Why Reducer required to use pure functions was also mentioned in the document and summarized as follows:

  • State is created according to reducer, so reducer is closely related to state. For state, we sometimes need to have some requirements (such as printing the state before and after each update or returning the state before an update), which has some requirements on reducer.

  • Pure functions are easier to debug

    • For example, when debugging, we want the action and the corresponding old and new state to be printed. If the new state is modified on the old state, using the same reference, then the old and new states cannot be printed.
    • If the output of a function is random, or depends on anything outside of it, it can make it very difficult to locate problems while debugging.
  • If we do not use pure functions, we will have to make deep comparisons when comparing the two objects corresponding to the old and new state, which is very wasteful of performance. On the contrary, if we create a new object and assign values to all objects that may be modified (for example, the reducer was called once, and the incoming state may be changed), the two objects have different addresses. Then a shallow comparison will do.

Now that we know that reducer is a pure function, what if we do need to deal with some side effects (asynchronous processing, API calls, etc.) in our application? This is what middleware solves. Let’s talk about middleware in Redux.

Mechanisms for middleware to handle side effects

We can see where the middleware is in redux by looking at these two figures.

Let’s take a look at the Redux workflow without middleware:

  1. Dispatch an Action (pure object format)
  2. This action was processed by the Reducer
  3. Reducer Update store (state in action)

With middleware, the workflow looks like this:

  1. Dispatch an “action” (not necessarily a standard action)
  2. This “action” is first handled by middleware (such as sending an asynchronous request here)
  3. When the middleware finishes processing, it sends an “action” (either the original action or a different action depending on the functionality of the middleware).
  4. An “action” issued by the middleware may continue to be processed by another middleware in a step similar to step 3. That is, middleware can be chained.
  5. After the last middleware process, dispatch an action (pure object action) that meets the reducer processing criteria
  6. This standard action is handled by the Reducer,
  7. Reducer Update store (state in action)

So how does middleware fit into Redux?

In the above flow, steps 2-4 are about middleware. Whenever we want to add a middleware, we need to write a set of logic 2-4.

If we need multiple middleware, we need to think about how to connect them together. If you write a concatenation logic for each concatenation, it is not flexible enough. If you need to add, delete, change or adjust the order of the middleware, you need to modify the logic of the middleware concatenation.

Therefore, Redux provides a solution that encapsulates the serial operations of the middleware. After encapsulation, steps 2-5 above can be integrated as shown in the following figure:

We just need to revamp the Store’s own dispatch method. When an action occurs, it is first processed by the middleware and finally dispatched to the reducer to change the state.

Use middleware in Redux

Remember enhancer, the third argument to Redux’s createStore() method:

function createStore(reducer, preloadedState, enhancer) {
    ifEnhancer is valid {return enhancer(createStore)(reducer, preloadedState)
    } 
    
    / /...
}
Copy the code

Here, we can see that an enhancer (which can be called a enhancer) is a function that takes a “normal createStore function” as an argument and returns a “enhanced createStore function”.

What this enhancement does is essentially transform the Dispatch and add middleware.

The applyMiddleware() method provided by Redux returns an enhancer.

ApplyMiddleware, as its name suggests, is “application middleware.” The input is several middleware and the output is enhancer. Here’s the source code:

function applyMiddleware(. middlewares) {
    // Returns A function A whose argument is A createStore function.
    // Function A returns function B, which is an enhanced createStore function. The body of function B is enclosed in braces
    return createStore= >(... args) => {// Create a store with the createStore parameter passed in
        conststore = createStore(... args)// Note that all we need to change here is the store dispatch method
        
        let dispatch = (a)= > {  // A temporary dispatch
            					// Calling Dispatch before the upgrade is complete will only print an error message
            throw new Error('Some error messages')}// Next we are going to associate each middleware with our state (by passing in the getState method) to get the transformation function.
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (. args) = >dispatch(... args) }// Middlewares is an array of middleware functions whose return value is a transform dispatch function
        // Call each middleware function in the array to get all the transformation functions
        const chain = middlewares.map(middleware= > middleware(middlewareAPI))
        
        // Compose these transformation functions into a function
        // Modify store dispatches with the compose functiondispatch = compose(... chain)(store.dispatch)// The compose method does something like this:
        // compose(func1,func2,func3)
        // Return a function: (... args) => func1( func2( func3(... args) ) )
        // That is, the incoming dispatch is transformed by func3 to get a new dispatch, and the new dispatch is transformed by func2...
        
        // Return to store and replace the dispatch in store with the modified dispatch method
        return {
            ...store,
            dispatch
        }
    }
}
Copy the code

To recap, applyMiddleware works like this:

  1. Call (several) middleware functions to get (several) transformation functions
  2. Compose all compose functions into one compose function
  3. Transform the Dispatch method

Middleware works like this:

  • Middleware is a function, so call it a middleware function
  • The input to the middleware function is storegetStateanddispatch, the output is transformation function (transformationdispatchThe function)
  • The transformation function input is adispatch, output “transformeddispatch

The source code uses a useful method, compose(), which combines multiple functions into a single function. Understanding this function is very helpful to understand the middleware, let’s have a look at its source:

function compose(. funcs) {
    // If no function is passed, return a function: arg => arg
    if(funcs.length === 0) {
        return arg= > arg
    }
    
    // When only one function is passed, the function is returned directly
    if(funcs.length === 1) {
        return funcs[0]}// Return the combined function
    return funcs.reduce((a, b) = >(... args) => a(b(... args)))// Reduce is a built-in method for js Array objects
    //array.reduce(callback) applies a callback function to each element in an array
    / / callback function:
    /* * @parameter {accumulator} : Callback return value of the last call * @parameter {value} : current array element * @parameter {index} : optional, current element index * @parameter {array} : Callback (Accumulator, value, [index], [array]) */
}
Copy the code

Draw a picture to understand compose’s role:

In the applyMiddleware method, the “parameter” we pass in is the original Dispatch method and the “result” we return is the modified Dispatch method. With Compose, we can abstract multiple transformation functions into a single transformation function.

Implementation of Middleware

Author’s note: I was just going to talk about Redux, but along the way I realized that understanding middleware is a prerequisite for understanding the middleware mechanism of Redux.

Let’s use Redux-Thunk as an example to see how a middleware implementation works.

The function of the story – thunk

You probably haven’t used redux-thunk, so before reading the source code, I’ll briefly explain what redux-thunk does:

An action argument to a normal Dispatch function should be a pure object. Like this:

store.dispatch({
    type:'REQUEST_SOME_THING'.payload: {
        from:'bob',}})Copy the code

With thunk, we can dispatch a function:

function logStateInOneSecond(name) {
    return (dispatch, getState, name) = > {  // This function dispatches a real action when appropriate
        setTimeout({
            console.log(getState())
            dispatch({
                type:'LOG_OK'.payload: {
                    name,
                }
            })
        }, 1000)
    }
}

store.dispatch(logStateInOneSecond('jay')) // The dispatch argument is a function
Copy the code

Why do you need this feature? Or what problem does “dispatch a function” solve?

As you can see from the above example, if we dispatch a function, we can do whatever we want inside that function (asynchronous processing, call interfaces, etc.) without any restrictions. Why?

Because we “didn’t dispatch a real action yet”, we didn’t call the Reducer, we didn’t put the side effects in the Reducer, but we dealt with them before using the Reducer.

If you still don’t understand what Redux-Thunk does, go to its Github repository for a more detailed explanation.

The realization of the story – thunk

How do you implement the Redux-Thunk middleware?

Firstly, the middleware must transform the Dispatch method, and the transformed Dispatch should have the following functions:

  1. If the argument passed is a function, the function is executed.
  2. Otherwise, we assume that we are passing in a standard action and call the “pre-dispatch” method, which dispatches the action.

Now let’s look at the source code for redux-thunk (8 lines of valid code) :

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) = > next => action= > {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}
Copy the code

If the three arrow functions make you a little dizzy, let me help you expand:

//createThunkMiddleware returns thunk middleware.
function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // This is "middleware function"
        
        return function(next) { // This is the "transform function" created by the middleware function.
            
            return function(action) { // This is the "dispatch method" after the transformation function.
                if (typeof action === 'function') {
                  return action(dispatch, getState, extraArgument);
                }
                
                returnnext(action); }}}}Copy the code

Some more notes?

function createThunkMiddleware(extraArgument) {
    
    return function({ dispatch, getState }) { // This is "middleware function"
        // The arguments are the dispatch and getState methods in store
        
        return function(next) { // This is the "transform function" created by the middleware function.
            // The next parameter is the dispatch before it was modified by the current middleware
            // Call it next because it may have been modified by other middleware before being modified by the current middleware
            
            return function(action) { // This is the modified function "modified dispatch method"
                if (typeof action === 'function') {
                  // If action is a function, call the function and pass in arguments to the function to use
                  return action(dispatch, getState, extraArgument);
                }
                
                // Otherwise call the pre-dispatch method
                returnnext(action); }}}}Copy the code

Finished. It can be seen that Redux-Thunk strictly follows the idea of redux middleware: Deal with the side effects before the reducer process is triggered by the original dispatch method.

conclusion

At this point, redux core source code has been finished, finally have to sigh, Redux write really beautiful, really TM concise.

Redux’s core functionality is summed up in one sentence: “Create a Store to manage State”

In terms of middleware, I will try to write an article called “How to Implement a Redux Middleware on my own” to get a deeper understanding of what Redux middleware means.

I’ll write another react-Redux source Code Interpretation blog to explore how store works with other frameworks such as React.

Stay tuned.


First Update 2018-09-12

  • Added tocompose()Source code interpretation
  • [Fixed] wrong description “The only way to change state is to trigger dispatch” Added information aboutunSubscribeThe interpretation of the
  • Added middleware implementation principles toredux-thunkAs an example