First of all, in the previous article Redux principle analysis has known a core process of ReUDx, and the principle, did not look at it does not matter.

Flow: Dispatch (action) = Dispatch ({type: “”, payload: {}}) -> Execute reducer() -> Modify state.

In other words, as long as I use Dispatch, I will directly execute Reducer to modify state, which is completely correct, so why do I need middleware?

The role of middleware: it is to do various processing between the source data and the target data, which is conducive to the extensibility of the program. Usually, a middleware is a function, and a middleware is best to do only one thing.

In Redux, the purpose of the middleware is to do some other operation before the dispatch is called to trigger the reducer, that is, it changes the process that executes the dispatch to trigger the reducer.

Here’s how it works:

This is what happens when you add middleware:

With an understanding of the redux middleware function and basic process, you can begin to analyze the redux middleware source code

Review the order in which functions are executed

It is necessary to review first. (At least I was wrong at first glance…)

What is the following output order?

 function fn1() {
   console.log(1)
 }
 ​
 function fn2() {
   console.log(2)
 }
 fn1(fn2())
Copy the code

The correct answer is 2 1, because fn2() returns the result as an argument to fn1. Therefore, fn2 needs to be executed first

Keep this execution order in mind for the following analysis!!

Redux middleware

Let’s take a look at how the redux middleware is written and see why.

function ({ dispatch, getState }) { return next => action => { next(action) }; } // Convert the arrow function and give it a name for easy observation. Read function mid1({dispatch, getState}) {return function fn1(next) {return function fn1Rf(action){next(action)}; }}Copy the code

Example:

import { createStore, applyMiddleware } from 'redux'; function countReducer(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; }} // middleware 1 function mid1({dispatch, GetState}) {return function fn1(next) {return function fn1Rf(action) {return next(action)}} mid2({ dispatch, }) {return function fn2(next) {return function fn2Rf(action) {return next(action)}}} // middleware 3 function mid3({ dispatch, getState }) { return function fn3(next) { return function fn3Rf(action) { return next(action) } } } function countReducer(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; Const store = createStore(countReducer, applyMiddleware(mid1, mid2, mid3)); export default storeCopy the code

createStore:

Focus only on the middleware flow, that is, if the preloadedState and enhancer must be passed.

If the second argument preloadedState is a function, then enhancer = preloadedState, otherwise we pass the third argument,

The second argument and the third argument cannot be functions at the same time.

 export default function createStore(
   reducer,
   preloadedState,
   enhancer
 ){
   if (
     (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
     (typeof enhancer === 'function' && typeof arguments[3] === 'function')
   ) {
     throw new Error('........')
     )
   }
 ​
   if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
     enhancer = preloadedState
     preloadedState = undefined
   }
   if (typeof enhancer !== 'undefined') {
     if (typeof enhancer !== 'function') {
       throw new Error('.......')
     }
     return enhancer(createStore)(
       reducer,
       preloadedState
     ) 
       // ...enhancer 为undefined 或者不为function 时的后续处理逻辑, 不是本文关键
   }

Copy the code

Returns:

 enehancer(createStore)(educer,preloadedState) 

Copy the code

Enehancer. If you run applyMiddleware(), enehancer is an enehancer

applyMiddleware

import compose from './compose' export default function applyMiddleware(... middlewares){ return (createStore) => (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) 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: (action, ... args) => dispatch(action, ... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch) return { ... store, dispatch } } }Copy the code

You can see that the applyMiddleware returns a value that does match the Enehancer execution

Enehancer (createStore)(educer,preloadedState) => (Reducer,preloadedState) => {}Copy the code

We have mainly done the following:

  1. Start by creating the Store using createStore
  2. Then execute middlewares. Map to provide getState and Dispatch methods for each middleware, This dispathch refers to the Dispathch of throw new Error, used as Error handling for calling Dispathch in middleware ** (not to be confused with store.dispatch) **.
  3. Middlewares. Map executes each intermediate first layer function, such as mid1. GetState and Dispatch can be used in each fn1 and fn1Rf function.
    function mid1({ dispatch, getState }) {  
        return function fn1(next) {  
            return function fn1Rf(action){ next(action) }; }} When the first layer of hou is executed and middlewares is passed into compose, there are only two layers per middleware.function fn1(next) {  
        return function fn1Rf(action){  
            next(action)  
        };  
    }
Copy the code
  1. Next, implement compose to compose all the middleware.

compose

The source code is as follows:

export default function compose(... Funcs) {// pass no arguments, 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

Let’s simplify and change the arrow function to make it easier to observe:

export default function compose(... funcs) { return funcs.reduce(function(a, b) { return function (... args) { return a(b(... args)) } }) }Copy the code

In this example, mid1, MID2, and MID3 are executed in applyMiddleware and only two layers are left. Both layers have access to Dispatch, and getState, as follows:

function fn1(next) { return function fn1Rf(action) { return next(action) } } function fn2(next) { return function fn2Rf(action) { return next(action) } } function fn3(next) { return function fn3Rf(action) { return next(action) } } // For example, const composeFn = compose(fn1, fn2, fn3) in applyMiddleware;Copy the code

The steps are as follows:

  1. A is fn1,

    B for the fn2,

    Returns:(... args) => fn1(fn2(... args))I’ll call it A here
  2. A is (… args) => fn1(fn2(… args)),

    B for fn3,

    return(... args) => ((... args) => fn1(fn2(... args)))(fn3(... args))I’ll call it B here

Step 1 returns the result equivalent to

function A(... args) { return fn1(fn2(... args)) }Copy the code

The second step returns the result equivalent to

A in step 2 is the result a returned from step 1, and a is self-executing in step 2

function B(... args) { return (function A(... args) { return fn1(fn2(... args)) })(fn3(... args)) }Copy the code

To self-execute A, fn3(… Args) is passed into fn2() as an argument to A

Simplify to:

function B(... args) { return fn1(fn2(fn3(... args))) } const composeFn = compose(fn1, fn2, fn3); // composeFn === B composeFn = function(... args) { return fn1(fn2(fn3(... args))) }Copy the code

The next step is to continue the analysis and execute the composeFn to get the final dispatch we need

Disptch, to avoid confusion with the final dispatch, Name it storeDispatch let storeDispatch = (action)=> {console.log(' modified successfully ')} // Execute composeFn Dispatch = composeFn(storeDispatch); // Get dispatch = fn1(fn2(fn3(storeDispatch)))Copy the code

After fn3,fn2 and fn1 are executed, we can get:

Dispatch = function fn1Rf(action) {return function fn2Rf(action) {return function fn3Rf(action) {// this storeDispatch Disptch return storeDispatch(action)}}}Copy the code

The middleware execution order inside the Redux is the reverse of the incoming order.

However, when the final composition is completed, the execution order when using Dispatch is consistent with the incoming order of the middleware.

Because a complete middleware is nested by three layers of functions!!

That explains the core principles of middleware

Tips for writing middleware

Under normal circumstances, the basic structure of a middle price, but to pay attention to the following three points

function mid1({ dispatch, getState }) { //1. Return function fn1(next) {//2. Return function fn1Rf(action) {//3. Using dispatch() causes an infinite loop next(action)}}}Copy the code

The reason:

The first and second reasons are as follows:

The Dispatch in the middlewareAPI in applyMiddleware refers to this dispatch function with throw new Error(), so using dispatch when composing middleware will throw an exception.

export default function applyMiddleware(... middlewares){ return (createStore) => (reducer, preloadedState) => { 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: (action, ... args) => dispatch(action, ... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch) } }Copy the code

Third reason:

dispatch = compose(... chain)(store.dispatch)Copy the code

The final result of this code combined with the middleware (analyzed above) :

Here are the results:

// Temporarily call mark dispatch = function fn1Rf(action) {// At this point dispatch() is executed for the third reason. This dispatch refers to mark, so it causes, Infinitely re-execute fn1Rf // dispatch(), Return function fn2Rf(action) {return function fn3Rf(action) {// This storeDispatch is compose(... Chain)(store.dispatch) is the incoming store.dispatch // to avoid confusion, use the storeDispatch name, Return storeDispatch(action)}}}Copy the code