Understanding “Section-oriented Programming” from the Realization Principle of Redux Middleware
This tutorial will analyze the implementation principle of Redux middleware by combining the Redux application example and applyMiddleware source code. On this basis, a preliminary understanding of the classical programming thought of “section-oriented” is established.
1. Know about Redux middleware
Before analyzing the implementation principle of middleware, let’s first understand the usage of middleware.
(1) Introduction of middleware
When we introduced the createStore function in Lecture 06, we briefly mentioned middleware — middleware related information is passed in as a function type entry to the createStore function. Here’s a quick review of the createStore call rules, with the following example code:
Redux import {createStore, applyMiddleware} from 'redux'...... Const store = createStore(Reducer, Initial_state, applyMiddleware(Middleware1, middleware2...) );Copy the code
As you can see, Redux exposes the applyMiddleware method. ApplyMiddleware accepts any middleware as an input, and its return value is passed to createStore as a parameter. This is middleware introduction.
(2) The working mode of middleware
How will the introduction of middleware change the Redux workflow? Using Redux-Thunk as an example, we’ll start with the classic “asynchronous Action” scenario and see how middleware solves the problem.
① Redux-thunk — classic asynchronous Action solution
In an analysis of the main flow of the Redux source code, it is not hard to see that there are only synchronous operations in the Redux source code, which means that when a Dispatch action is performed, the state is updated immediately.
So what if you want to introduce asynchronous data flows in Redux? The official recommendation from Redux was to use middleware to enhance createStore. There are many Redux middleware that support asynchronous data streams, but the best one to get started with is **redux-thunk**.
The introduction of Redux-Thunk is similar to normal middleware, as shown in the following example:
Redux-thunk import thunkMiddleware from 'redux-thunk' import Reducer from './reducers Const store = createStore(Reducer, applyMiddleware(thunkMiddleware))Copy the code
A quick refresher. In Lecture 19, when analyzing the entire createStore source code, I saw this code at the beginning of the createStore logic:
// The initial state is not set. If (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {// The second parameter is considered to be enhancer = preloadedState; preloadedState = undefined; }Copy the code
As you can see from this code, if only two parameters are passed in, createStore will check if the second parameter is of type function, and if so, will consider the second parameter “enhancer”. “Enhancer” means “enhancer,” and applyMiddleware’s packaged middleware is one of those enhancers. This explains why in the redux-Thunk call example above, the applyMiddleware call was passed in as the second parameter to createStore, but was still recognized as middleware information.
The change to redux-thunk is easy to understand. It allows you to distribute an action as a function, like this:
Import axios from 'axios' // Import createStore and applyMiddleware import {createStore, applyMiddleware } from 'redux'; Redux-thunk import thunk from 'redux-thunk'; Reduce import reducer from './reducers'; Const store = createStore(Reducer, applyMiddleware(thunk)); // It is used to initiate a payment request and process the result of the request. Because of the money involved, // Note that the return value of payMoney is still a function const payMoney = (payInfo) => (dispatch) => {// Dispatch ({type: 'payStart' }) fetch().then(res => { dispatch()}) return axios.post('/api/payMoney', { payInfo }) .then(function (response) { console.log(response); Dispatch ({type: 'paySuccess'})}). Catch (function (error) {console.log(error); Dispatch ({type: 'payError'})}); } const payInfo = {userName: XXX, password: XXX, count: XXX,...... } // dispatch an action, notice that this action is a function store.dispatch(payMoney(payInfo));Copy the code
Redux-thunk is used to simulate the initiation – response process of a payment request.
On the surface, the biggest difference between this process and a normal Redux call is that the input parameter to Dispatch is changed from an Action object to a function. This makes you wonder about the Thunk middleware enabled Redux workflows — the action entry must be an object, as we saw when we analyzed the Dispatch source code in part 20. Thunk middleware seems to circumvent this check, but what’s behind it?
To understand this, in addition to understanding thunk’s execution logic, it’s important to understand how the Redux middleware works.
② How does Redux middleware integrate with Redux main process?
The Redux middleware will execute the actions after they are distributed but before they arrive at reducer. Corresponding to the workflow, its execution time is as shown in the figure below:
If there are multiple middleware components, Redux will invoke them in the order in which they were “installed,” as shown below:
The timing of the execution of the middleware allows it to do what it wants with the action information before the state actually changes.
So how does middleware “bypass” dispatch’s validation logic? In fact, “bypassing” dispatch is just a subjective use experience. Dispatch isn’t bypassed, it’s overwritten, and it’s being overwritten by none other than applyMiddleware. This will be covered further in the source code analysis section later in this article.
At this point, there are two things to keep in mind about how Redux middleware works:
-
The execution time of middleware, that is, after action is distributed and before reducer trigger;
-
The execution premise of middleware, that is, applyMiddleware will rewrite the Dispatch function so that before dispatch triggers reducer, chain calls to Redux middleware will be performed first.
Combine these two points and look at the source code for Redux-Thunk, and everything falls into place.
What does Thunk middleware really do?
The source code for redux-Thunk is actually very concise, and although many things have changed over the years, you can find that redux-Thunk is still very straightforward. Redux-thunk redux-thunk
CreateThunkMiddleware (extraArgument) {return value is a thunk, Return ({dispatch, getState}) => (next) => (action) => {// thunk Action if (typeof Action === 'function') {return action(dispatch, getState, extraArgument); } return next(action) if action is not a function; }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;Copy the code
The main thing redux-thunk does, after intercepting an action, is to check if it is a function. If action is a function, redux-thunk executes it and returns the result; If action is not a function, then it is not the target of redux-thunk. Call Next, tell Redux “I’m done here,” and the workflow can move on.
At this point, you have enough knowledge of the Redux middleware at the usage level. Next, enter the world of source code ~
How is the Redux middleware mechanism implemented
Redux middleware was introduced by calling applyMiddleware, so take a look at the source code for applyMiddleware (explained in the comments) :
// applyMiddlerware uses "..." Export Default Function applyMiddleware(... Middlewares) {// It returns a function that receives createStore as an input parameter. Return createStore => (... Args) => {// First call createStore, create a store const store = createStore(... args) 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(... } // iterate through the middleware array, call each middleware, and pass in the middlewareAPI as an input parameter, Middlewares. map(Middleware => Middleware API) const chain = middlewares.map(middlewareAPI) "Compose" the chain functions in order, call the final combined function, pass in dispatch as the input argument dispatch = compose(... Chain)(store.dispatch) // Returns a new store whose dispatch has been overwritten. Return {... store, dispatch } } }Copy the code
In this section of source code, the key issues to be clear are the following:
-
What function does applyMiddleware return? How does this function work with createStore?
-
How was the dispatch function overwritten?
-
How does the compose function compose middleware?
(1) How applyMiddleware works with createStore?
Take a look at the return value of applyMiddleware. As noted in the source code comments, it returns a function that accepts createStore as an input parameter. This function will be passed to createStore as an input parameter, so how will createStore interpret it? To review the enhancer logic in createStore, see the following code:
Function createStore(reducer, preloadedState, enhancer) { If (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {// The second parameter is considered to be enhancer = preloadedState; preloadedState = undefined; } if (typeof enhancer! == 'undefined') { return enhancer(createStore)(reducer, preloadedState); }... }Copy the code
As you can see from this code snippet, once an enhancer is found (in the middleware scenario, enhancer refers to the function returned by applyMiddleware), CreateStore will then return a call directly to enhancer. In this call, the first input parameter is createStore and the second input parameter is Reducer and preloadedState.
Try counterbalancing this logic in applyMiddleware. The framework for applyMiddleware is as follows:
// applyMiddlerware uses "..." Export Default Function applyMiddleware(... Middlewares) {// It returns a function that receives createStore as an input parameter. Return createStore => (... args) => { ...... }}Copy the code
The createStore entry corresponds to the createStore function itself in applyMiddleware return. The arGS input parameter corresponds to Reducer and preloadedState, both of which are the agreed input parameters of createStore function.
As mentioned earlier, applyMiddleware is a type of enhancer, and enhancer means “enhancer,” which enhances createStore’s ability. Therefore, it is necessary to pass in the createStore and its associated input information when calling enhancer.
(2) How is the dispatch function rewritten?
The rewrite of the Dispatch function is done by the following code fragment:
Const middlewareAPI = {getState: store.getState, dispatch: (... args) => dispatch(... } // iterate through the middleware array, call each middleware, and pass in the middlewareAPI as an input parameter, Middlewares. map(Middleware => Middleware API) const chain = middlewares.map(middlewareAPI) "Compose" the chain functions in order, call the final combined function, pass in dispatch as the input argument dispatch = compose(... chain)(store.dispatch)Copy the code
This code fragment does two things: first, it calls the middlewareAPI as an input, one by one, to get an array of “inner functions” called chain; The compose function is then called, grouping the “inner functions” in the chain one by one, and calling the resultant function.
In the above description, the following two points may constitute a barrier to understanding:
-
What is an “inner function”?
-
The compose function is composed in the following way: And what does it put together?
For point 2, go to the compose source code and press the not list first. Let’s first look at what “inner function” means here.
First we need to stand in the perspective of the function, to observe the source code of thunk middleware:
CreateThunkMiddleware (extraArgument) {return value is a thunk, Return ({dispatch, getState}) => (next) => (action) => {// thunk Action if (typeof Action === 'function') {return action(dispatch, getState, extraArgument); } return next(action) if action is not a function; }; } const thunk = createThunkMiddleware();Copy the code
Thunk middleware is the return value of createThunkMiddleware, which returns a function like this:
({dispatch, getState}) => (next) => (action) => {// thunk Action if (typeof Action === 'function') {return action(dispatch, getState, extraArgument); } return next(action) if action is not a function; };Copy the code
The return value of this function is still a function, which is obviously a higher-order function. In fact, by convention, all Redux middleware must be higher-order functions. In higher-order functions, we are used to calling the original function “outer function” and the return function “inner function”.
Apply iterates through the Middlewares array and calls middleware(middlewareAPI) one by one to get the middleware’s inner functions.
Using the thunk source code as an example, you can see that the main purpose of the outer function is to get the DISPATCH and getState apis, while the real middleware logic is wrapped in the inner function. Middlewares. Map (Middleware => Middleware API) is executed, and all the inner functions are extracted into the chain array. Now, let’s just do the chain array.
The first thing applyMiddleware does after extracting the chain array is compose the middleware logic from the array.
So how does the compose function work?
(3). Compose source code interpretation: function composition
Function composition (combinatorial functions) is not the exclusive property of Redux, but is a general concept in functional programming. Therefore, in the Redux source code, the compose function exists as a separate file with strong utility properties.
Start by reading the source code to find out what Compose does. Here is the source code for compose (explained in the comments) :
// compose first uses "..." The operator converges the input parameter to the array format export default function compose(... Funcs) {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
In fact, the entire source code is worth perusing only this last line of code:
Return funcs.reduce((a, b) => (...) return funcs.reduce((a, b) => (... args) => a(b(... args)))Copy the code
This line of code tells us that function composition is achieved by calling the reduce method of the array.
Reduce method is a relatively basic concept in JS array, which will not be explained here. If you need to review, please click here.
The reducer method is characterized by executing our specified function logic on each element in the array and summarizing its results into a single return value. So for a compose call like this:
compose(f1, f2, f3, f4)
Copy the code
It will combine functions into this form:
(... args) => f1(f2(f3(f4(... args))))Copy the code
In this way, the inner logic of F1, F2, F3 and F4 middleware will be combined into a function. When this function is called, F1, F2, F3 and F4 will be called in sequence. That’s what “combination of functions” means here.
4. Middleware and faceted programming
The concept of middleware is not a patent of Redux. It has a long history in the software field. The well-known Node frameworks such as Koa and Express also have many applications for middleware. So why is middleware popular? Why do our applications need middleware? Here, take Redux middleware mechanism as an example, briefly talk about the “aspect oriented” programming thought behind middleware.
AOP (aspect oriented) is a concept that many people may not be familiar with, but OOP (object oriented) is relatively familiar with. AOP exists precisely to solve OOP’s limitations, and we can see AOP as a complement to OOP.
In OOP mode, when we want to extend the logic of A class, the most common idea is inheritance: class A inherits class B, and class B inherits class C…… This passes logic down layer by layer.
When you want to add a piece of common logic to several classes, you can do this by modifying their common parent class. This will undoubtedly make the common class more and more bloated, but there is no better way to do this — you can’t just leave the common logic scattered among different business logic. That leads to more serious code redundancy and coupling problems.
What to do? “Face the section” to the rescue!
Since it is facing the “section”, it is necessary to figure out what the “section” is first. A section is a concept relative to an execution process. In the case of Redux, the workflow would look like this from top to bottom, as shown below:
Consider the requirement that after each Action is dispatched, a console.log is printed to record that the Action was dispatched, also known as “log tracing.” This requirement is too generic and weak in business attributes to be coupled to any business logic. This can be separated from the business logic in the form of “facets” : the execution node of the extension function in the workflow can be regarded as a single “cut point”; We put the logic of extension functions on this “cut point”, forming a “cut surface” that can intercept the preceding logic, as shown in the figure below:
Aspects are separate from business logic, so AOP is typically a “non-intrusive” way of thinking about logical extensions.
In daily development, functions such as “log tracing”, “asynchronous workflow processing”, and “performance dogging”, which are not related to business logic, can be considered to be pulled into “facets”.
The benefits of faceted programming are clear. From the middleware mechanism of Redux, it is not difficult to see that the aspect oriented idea greatly improves the flexibility and cleanliness of our organizational logic, and helps us avoid the problems of logic redundancy and logic coupling. By separating the “facets” from the business logic, developers can focus on developing the business logic and have the freedom to organize their desired extensions in a “plug and play” way.
5, summary
In this lecture, redux-Thunk middleware is firstly taken as an example to understand the working mode of Redux middleware from the “asynchronous workflow” scenario. Then, combined with applyMiddleware source code, the whole implementation mechanism of Redux middleware is analyzed in detail, and the “section oriented” programming idea is introduced at the end of the article.
At this point, the entire core knowledge system led by Redux has been shown in a glance in front of you, AND I believe that everyone’s understanding of Redux has reached another level.
In the next lecture, we will use React Router, another useful helper of React, as a starting point to explain front-end routing knowledge.
Learning the source (the article reprinted from) : kaiwu.lagou.com/course/cour…