Module 3: The surrounding ecology helps you broaden your technical horizon
Redux Design Philosophy and Working Principle revealed (PART 1)
I’m sure you’ve all had some experience with Redux, and the basics of Redux were laid out in Lecture 06. Starting with this lecture, we will build on this to study Redux in a more systematic and in-depth way.
[Note] : If you haven’t touched Redux before, click here to get started quickly.
What is “systematic” learning? One premise of the system is to establish the necessary learning context, to try to understand the context of things.
It has been common over the years to see people who are familiar with the basics of Redux and even understand how it works, but just don’t understand why Redux is used, let alone what problem Redux solves. So before we talk about the source code and principles, it’s important to clarify the background and architectural ideas behind Redux.
1. The architectural idea behind Redux — understand the Flux architecture
Redux’s design largely benefits from the Flux architecture, and Redux can be considered an implementation of Flux (although it does not strictly follow Flux’s Settings), and understanding Flux will help us better grasp Redux at an abstract level.
Flux is not a concrete framework, but a set of application architectures developed by Facebook’s technical team that constrain the way applications process data. In the Flux architecture, an application is split into the following four parts.
-
View (View layer) : User interface. The user interface can be implemented in any form, the React component is one form, and Vue and Angular are all right. There is no coupling between the Flux architecture and React.
-
Action: Also known as a “message” sent by the view layer that triggers a change in application state.
-
Dispatcher: It is responsible for the distribution of actions.
-
Store (data layer) : It is the “repository” for storing application state, in addition to defining the logic for changing state. Store changes are eventually mapped to the View layer.
The collaboration between these four parts will be accomplished through the workflow rules shown below:
A typical Flux workflow looks like this: a user interacts with a View and initiates an Action through the View; The Dispatcher sends this Action to the Store to inform the Store of the appropriate status update. After the Store status update is complete, the View will be further notified to update the interface.
It is worth noting that all the arrows in the figure are one-way, which is a core feature of the Flux architecture — one-way data flow.
So what problem is the Flux architecture designed to solve?
2. What problem does the Flux architecture solve
The core feature of Flux is one-way data flow. To fully understand the benefits of one-way data flow, it is necessary to first understand the problems caused by two-way data flow.
(1) Limitations of MVC mode in front-end scenarios
The most typical representative of two-way data flow is the MVC architecture in the front-end scenario, which is shown in the diagram below:
In addition to allowing the user to trigger the process through the View layer interaction, there is another form of MVC architecture that allows the user to trigger the process by directly triggering the Controller logic. The architectural relationship in this mode is shown below:
In an MVC application, there are three parts involved:
-
Model, the data or information that the program needs to manipulate;
-
View, user interface;
-
Controller, which connects the View to the Model and manages the logic between the Model and View.
In principle, the relationship between the three should be similar to the one shown above. After the user manipulates the View, the Controller processes the logic (or directly triggers the Controller’s logic), and the Controller applies the changes to the Model and finally feeds back to the View. In this process, the data flow should be one-way.
In fact, in many server-side MVC applications, data flow does remain one-way. However, in the front-end scenario, the actual MVC application is much more complex. The front-end application/framework often allows the View and Model to communicate directly for the need of interaction. The architectural relationship should look like this:
This allows two-way data flow. When business complexity is high, the data flow can become very chaotic, with a situation like the following:
The example in the figure only has one Controller, but given that there can be multiple controllers in an application, it should be much more complicated than the figure above (although the figure itself is complicated enough).
With such complex dependencies, even small project changes carry significant risks — a small change can have a “butterfly effect” on the entire project. With such confusion about the source of the changes, we can’t even start Bug checking because it’s hard to tell which Controller or View is causing a data change.
Looking back at Flux’s architectural pattern at this point, it’s more or less fascinating. To review the data flow patterns in Flux, see the following figure:
The core of Flux lies in the strict one-way data flow, under which the change of state is predictable. If the data in the Store changes, there is one and only reason that it is triggered by the Dispatcher sending Action. In this way, messy data relationships are fundamentally avoided and the whole process is made clear and simple.
But that doesn’t mean Flux is perfect. In fact, Flux’s constraints on data flow have a non-negligible cost behind them: In addition to higher learning costs for developers, Flux architecture also means an increased amount of code in a project.
The Flux architecture’s advantages and necessity are often found in “complex projects”. If the data relationships in the project are not complicated, Flux will not come into play at all, and this is the same for Redux.
Now let’s take a look at this definition from Redux, taking into account the nature of the Flux architecture:
Redux is a JavaScript state container that provides predictable state management.
At this point, I think I can understand the meaning behind the three words “predictable”.
3. Redux key elements and workflow review
The Redux library and the Flux architecture are mutually exclusive, although Redux is not implemented according to Flux (for example, Flux allows many stores while Redux has only one Store). But Redux does follow Flux in design thinking.
The characteristics of the Flux architecture, problem-solving ideas, and usage scenarios described above can be migrated to Redux. To understand Redux as a landing product in the context of Flux’s thinking, our learning curve will be smoother.
Before getting into how Redux works, let’s briefly review its key elements and workflow. Redux consists of three parts: Store, Reducer, and Action.
-
Store: It is a single data source and is read-only.
-
I’m going to take Action. I’m going to take Action.
-
Reducer is a function that distributes and processes changes and ultimately returns new data to the Store.
The close cooperation between Store, Action and Reducer forms a unique workflow of Redux, as shown in the figure below:
Throughout the work of Redux, the data flow is strictly one-way. If you want to make changes to your data, there’s only one way to do it: send out actions. Actions will be read by Reducer, and Reducer will perform different calculation logic according to different Action contents, and finally generate new state (state), which will be updated into the Store object and drive corresponding changes at the view level.
For components, any component can read the global state from the Store in a convention, and any component can modify the global state by reasonably distributing actions. Redux allows data to move freely and orderly between arbitrary components by providing a unified state container.
After reviewing the workflow of Redux, let’s look at the source code to see how this workflow is implemented.
4. How does Redux work
Take a look at the Redux source folder structure, as shown below:
Utils is the tool method library. Index. js is used as an entry file for convergence and export of function modules. The real “work” is the function module itself, which is the following files:
-
applyMiddleware.js
-
bindActionCreators.js
-
combineReducers.js
-
compose.js
-
createStore.js
ApplyMiddleware is a middleware module that is more self-contained and will be covered separately in Lecture 21.
BindActionCreators, which combines the incoming actionCreator with the dispatch method into a new methodology, The three methods, combineReducers (for combining multiple reducer functions) and compose (for combining received functions from right to left) are all tools.
If you are unfamiliar with these three tools and methods, don’t be in a hurry to search for them, as they are independent of the main Redux process and are “optional” auxiliary apis, and not being familiar with them does not affect understanding of Redux itself. To understand how Redux works, there’s only one module you really need to focus on — createStore.
The createStore method is the first method invoked when Redux is used. It is the entry point to the process and the core API in Redux. The next step is to start with createStore and figure out the main flow of the Redux source code.
(1) Beginning of the story: createStore
The first step in using Redux is to call the createStore method. From a pure sense of use, what this method seems to do is create a store object like this:
Import {createStore} from 'redux' const store = createStore(Reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) );Copy the code
The createStore method can accept the following three inputs:
-
reducer
-
Initial State (preloadedState)
-
Specifying middleware (enhancer)
So what happens between getting the input and getting back out of the store? Here is the source code for the body logic in createStore (interpreted in the comments) :
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); } // record the currentReducer, because replaceReducer modifs the reducer content. Let currentReducer = reducer; Let currentState = preloadedState; Notice currentListeners = []; // The listeners are now on the same note; // This variable is used to record whether dispatch let isDispatching = false // This method is used to check that snapshots are copies of currentListeners. Rather than the function the currentListeners ensureCanMutateNextListeners () {if (nextListeners = = = currentListeners) { nextListeners = currentListeners.slice(); Function getState() {return currentState; } // subscribe Function subscribe(listener) {// check the typeof listener if (typeof listener! == 'function') {throw new Error('Expected the listener to be a function.') (isDispatching) { throw new Error( 'You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.')} // This variable is used to prevent multiple calls to the unsubscribe function let isSubscribed = true; / / make sure nextListeners does not point to the same reference with currentListeners ensureCanMutateNextListeners (); // Register a listener function nextListeners. Push (listener); Return function unsubscribe() {if (! isSubscribed) { return; } isSubscribed = false; ensureCanMutateNextListeners(); const index = nextListeners.indexOf(listener); // Remove the current listeners from the nextListeners array. }; Function dispatch(action) {// Check whether the data format of the action is valid if (! isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' If (typeof action.type === 'undefined') {throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant? ')} // If you are already in the process of dispatch, If (isDispatching) {throw new Error('Reducers may not dispatch actions.')} try {// Execute the reducer Before, "lock" is used to mark that the dispatch execution process already exists. IsDispatching = true // Call reducer, CurrentState = currentReducer(currentState, action)} finally { // Constant listeners = (currentListeners = nextListeners); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } return action; } // replaceReducer you can change the currentReducer function replaceReducer(nextReducer) {currentReducer = nextReducer; dispatch({ type: ActionTypes.REPLACE }); return store; } // initialize state. When an action of type actiontypes.init is dispatched, each reducer returns // its initial dispatch({type: actiontypes.init}); Observable method can be ignored, it is used inside redux, // Wrap the defined method in a store object. Return {dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }Copy the code
Reading the source code, createStore looks like a simple creation action on the outside, but on the inside, it covers all of the core method definitions in the main Redux process.
The internal logic of createStore is summarized in a large diagram that covers the work of each of the core methods and will help you quickly understand the logical framework of createStore.
Among the createStore export methods, there is a strong correlation with the Redux main process, as wellSome of the most commonly used methods, respectively:
-
getState
-
subscribe
-
dispatch
Among them, the source content of getState is relatively simple, and it has been fully understood in the process of line by line analysis. Subscribe and Dispatch represent Redux’s unique publish-subscribe model and the most critical distribution actions in the main process, which we’ll focus on in the next lecture.
5, summary
In this lecture, I first learned the architectural idea of Redux, sorted out the background of the core feature of “one-way data flow”, and truly understood the meaning behind the three words “predictable” in Redux definition.
Then, on the basis of reviewing the key elements and workflow of Redux, we tried to disassemble its source code, understand the basic structure and main modules of Redux source code, and select the core module createStore as the focus. We extracted two methods in the Redux source code that are particularly worth digging into — subscribe and Dispatch.
So what is it about Subscribe and Dispatch that is worth learning more about? You’ll find out in the next lecture!
6, the appendix
To study the source