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:
- Start by creating the Store using createStore
- 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) **.
- 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
- 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:
- A is fn1,
B for the fn2,
Returns:(... args) => fn1(fn2(... args))
I’ll call it A here - 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