This article focuses on the architectural ideas and middleware model behind Redux
The Flux architecture idea behind Redux
Redux is an implementation of Flux (although it doesn’t strictly follow Flux). Understanding Flux will help you better understand Redux at an abstract level.
First, the Flux architecture divides an application into four parts:
- View: the view layer
- Action: Action issued by the view layer, such as mouseclick
- Dispatcher: Accepts actions and executes callbacks
- Store (data layer) : Stores the status of the application and alerts the View layer to update it when changes occur
As can be seen from the figure, Flux is characterized by ———— one-way data flow
- User access view
- The view issues the user’s action
- When the Dispatch receives the action, it asks the Store to update the data
- A change event is issued after the store is updated
- The change view update was received
I’m sure you read this and you say, okay? That’s why we’re doing it this way, because most front-end projects are MVC or MVVM architectures. What are the drawbacks of this architecture? One big drawback is that as business complexity becomes more and more complex, it allows direct transfer between the View layer and the Model layer. Because the Model might correspond to more than one view layer, something like this would happen:
The data flow is really confusing from the diagram, and if there is a bug in the project it is difficult to pinpoint where the problem is, so Flux at its core is a one-way data flow because view updates are notified from the Store.
The architecture of Redux is very similar to Flux: if you understand Flux, you will understand Redux, after all, the overall architecture idea is very similar. Without further ado, go straight to the picture:
I’ll give you a manual simulation of the whole process. The user clicks the mouse and sends an action, which is processed by Actions. The Actions actually return an object, which contains the type and required data. I don’t do any logical operations. After returning a new State, it is passed to the data center Store, which informs the view to update. That’s the end of the process. So Redux in the end is how to do, with curiosity, I and everyone together one source code, see him inside exactly what secret?
Redux source code parsing
The directory structure of the Redux source code is quite simple. Types mainly contains the types defined by TS. Utils is mostly generic, with nothing to do with key processes. So next, I will mainly analyze the three TS files of applyMiddleware, combinReducers, compose and createStrore
To start createStore with Redux, we first analyze this file
CreateStore
Import {createStore} from 'redux' // createStore const store = createStore(reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) );Copy the code
CreateStore accepts three parameters from the figure
- The first parameter is a reducer, a pure function, which we define ourselves
- The number state initialized by the second parameter
- The third parameter that defines the middleware in the source code is the enhancer enhancement store
What happens between getting the input and getting back out of the store? Here I extract the source code for the createStore body logic for you (parsed in the comments) :
This code is mainly doing some type determination, and it’s ok to be compatible with some writing. Moving on, here are some of the initial state assignments:
I’m sure some of you have questions about making sure that the snapshot and the shallow copy are different references, right? I’m here to sell a close, wait for the whole process to go through the back focus on the analysis why?? The next step is the getState function that we often use.
getState:
I grass so a few lines of code, very simple, the source code is that way, easy easy! Keep reading
The subscribe:
We made a shallow copy of the feed and uninstalled it with nextListeners, but remember we have a currentListeners, so it doesn’t work. Let’s move on.
dispatch:
At dispatch: Next is copied back to current and each listenr is executed. Now I think you understand that I dispatch in Reducer, right? Or to subscribe or to do some dirty operations, in the Redux source code to prevent this is to set the variable isDispatching to control.
So dispacth an action? What Redux did for us, it was two simple things
- OldState is generated by the Reducer newState and the Store data center is updated
- Trigger the subscription
The entire Redux workflow is over here, but we still have a question: why subscribe to nextListeners and then reassign to currentListeners in the Dispatch? Why is that?
The answer is: to ensure the stability of triggering subscriptions
Let me give you an example:
A function listenera() {} Const unSubscribea = store.subscribe(listenera) // define the listener function b function listenerb() {// unSubscribea in b C function listenerc() {} // Subscribe to b store.subscribe(listenerb) // subscribe to c store.subscribe(listenerc)Copy the code
From the above I can see the current currentListeners:
[listenera, listenerb, listenerc]
But the special thing is that listenb actually unloads listena, OK, if we don’t do a shallow copy, then when we trigger the subscription, the array is undefined when I = 2, and that causes an error, because we do a shallow copy before we subscribe and we unload the subscription, The nextListeners data changes as long as the currentListener is stable.
After the current dispacth, the next dispacth assumes no new subscriptions, and the data relationship is assigned again.
Listeners = (currentListeners = nextListeners)
It all makes sense when you think about this change, which is why Redux subscriptions are stable. Design is really very clever wow, read now found that the source is actually not imagined hot yao difficult? Full marks for detail processing. The next step is to analyze Redux’s middleware model.
Middleware ideas in Redux
To understand redux’s middleware thinking, let’s take a look at what the compose file does, which is pretty simple. It mainly uses the combination concept in functional programming to combine multiple function calls into a single function call.
In fact, the main API is reduce, for the sake of understanding, I will simply write array Reduce implementation:
Ok, this thing is actually an accumulator in one way or another. We’re going to go straight into the compose function and look at the code without saying a word:
Export default function compose(...) If (funcs. Length === 0) {return <T>(arg: If (funcs. Length === 1) {return funcs[0]} 👌 return funcs.reduce((a, b) => (... args: any) => a(b(... args)))}Copy the code
Let’s say we have these three functions:
funcs = [fa, fb, fc]
Since there is no initial value accumulator is FA after an accumulator traversal, accumulateur becomes the following appearance:
Let m = (… The args) = > fa (fb (… args))
After an accumulateur traversal, at this time B = FC
* * (... The args) (fc (= > m... args))**
We are going to fc (… Args as a function of m up here so it’s going to look like this
(… The args) = > fa (fb (fc (… args)))
OK, so we’re done here. Fa, FB, FC we can think of as 3 middleware of Redux, in order,
When this function is called, it will be called in the order fa, FB, fc. You know what? Just start with the analysis, right? How is compose combined with Redux?
applyMiddleWare
I first analyze the overall structure, and function parameters are divided into:
// applyMiddlerware will use "..." Export default function applyMiddleware(... Middlewares) {// It returns a function that takes createStore as an input return createStore => (... args) => { ...... }}Copy the code
CreateStore is the creation data center Store we analyzed above, and args mainly has two, or createStore has two convention input parameters: Reducer and initState.
enhance-dispatch
And then the core, rewrite Dispacth, why do you rewrite Dispatch, let me give you an example. If you remember, dispacth can only accept an action that is an object. If it is not an object, it will report a type error. As shown in figure:
The redux-Thunk middleware known in the OK community, Dispatch can accept a function, did she bypass our checks, but certainly not, computers can’t cheat. There’s only one reason.
Dispatches that use middleware and dispatches that don't use middleware are definitely not the same,
Dispatch = enhancer(dispacth) is definitely enhanced.
const store = createStore(... args) let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... Args)} chain = middlewares. Map (middleware => Middleware (middlewareAPI)) // Bind {dispatch and getState} dispatch = compose(... chain)(store.dispatch)Copy the code
The first step you can see in the code is actually to use getState and Dispatch as closures in the middleware. Some people ask here, why are these anonymous functions? Not Store.dispatch? Here you can first 🤔 below, answer in the back.
I think a lot of people are still confused by this, but just to give you an example, remember fa, Fb, FC and I’m going to expand them out.
Function fa(store) {return function(next) {return function(action) {console.log('A middleware1 start '); Next (action) console.log('B middleware1 end '); }; }; } function fb(store) {return function(next) {return function(action) {console.log('C middleware2 start '); Next (action) console.log('D middleware2 end '); }; }; } function fc(store) {return function(next) {return function(action) {console.log('E middleware3 start '); Next (action) console.log('F middleware3 end '); }; }; }Copy the code
Ok, let’s go step by step and see what we did.
chain = middlewares.map(middleware => middleware(middlewareAPI))
Copy the code
Middlewares = [fa, Fb, FC] map and return a new chain
chain = [ (next)=>(action)=>{…}, (next) => (action) => {…}, (next) => (action) => {…} ]
It’s just that every element in the chain has a closure of getSate, Dispatch.
Ok, and then we go down to the compose function remember up here, compose(fa, fb, fc), what’s the return value?
(… The args) = > fa (fb (fc (… args)))
That’s right, that’s the same thing, we have the same chain here, so dispatch here becomes enhanced Dispatch so let’s see
dispatch = fa(fb(fc(store.dispacth)))
See someone here and ask? What’s the difference between each middleware next and the original store.dispacth? What does this have to do with the onion model?
So I’m going to take you step by step, where dispatch is the current fa(FB (FC (store.dispatch))), we can directly call the function to analyze,
The value of fa is fb(fc(store.dispatch)), and the value of fb depends on the value of fc(store.dispacth). The value of FB depends on the value of FC (store. So we can see from the above process that next is actually a side effect of his last middleware, the last middleware next is **** store.Dispatch.
Side effect: (action) in each middleware => {… }
I use a flowchart to represent the entire process of the Onion model:
So when I call Dispatch, I print E, and then I find out next is fb, and then I call FB, and I print C, and then I find out next is FC, and then I call FC, and I print A, Next is store. Dispacth, the call ends, prints B, then D, and finally F. Such a series of operations is not a bit like the onion model, if I can layer by layer to open your heart, ha ha ha, too SAO, good return to the theme. Remember the question I asked earlier?
1. Why is Dispacth anonymous?
2. Why dispacth an action and return an action
Question 1: The reason why dispatch is an anonymous function is that some middleware implementations do not implement the next(action). In this case, the enhanced dispacth, redux-thunk implementation principle is that when you pass a function, call the function directly, And leave dispatch access to you.
function createThunkMiddleware(extraArgument) { return ({ dispatch, GetState}) => next => action => {if (typeof action === 'function') { Return Action (Dispatch, getState, extraArgument); // If store.dispatch is used, return action(Dispatch, getState, extraArgument); } return next(action); }; }Copy the code
Here’s the source code:
// Let dispatch is a closure: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' )}const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ... args) => dispatch(action, ... Args)}const chain = middlewares.map(Middleware => Middleware (middlewareAPI))// compose compose<typeof dispatch>(... chain)(store.dispatch)Copy the code
So Redux is not strictly an Onion model, his onion model is built on each of your middleware, next(action); If you don’t do the next(action), you actually break the onion model.
Issue 2: Dispatch (Action) returns an action to facilitate processing by the next middleware.
Here, Redux source code main process has been all analyzed, where the article is wrong, welcome to correct communication.
reference
Learn how to React. – Learn how to React
ApplyMiddleWare analysis