React is widely used in large-scale enterprise projects. When it comes to React, Redux is bound to be unavoidable. As a derivative library of React, Redux has solved the data problem quite effectively. Let’s take a look at Redux from a different point of view and give you a rough idea of how to use Redux
Redux says it all boils down to four key points:
Redux provides support for each knowledge point, so in this blog post, we’ll break it down into the following sections:
- handwritten
createStore
- handwritten
bindActionCreators
- handwritten
combineReducers
- handwritten
applyMiddleware
Before we do that, let’s do some preparatory work
Let’s create a new redux directory, create a few files and index.js, and export the other files in index.js by default
The current general directory structure is as follows
| - redux | -- index. Js as export,Export by default
|-- createStore.js # Logic used to implement createStore
|-- bindActionCreators.js # Implement Redux's bindActionCreators feature
|-- combineReducers.js # combineReducers for Redux
|-- applyMiddleware.js # applyMiddleware for Redux
|-- compose.js Compose function for redux
|-- utils.js # Mainly store some tool methods
Copy the code
// index.js
export { default as createStore } from "./createStore.js";
export { default as bindActionCreators } from "./bindActionCreators";
export { default as combinReducers } from "./combinReducers";
export { default as applyMiddleware } from "./applyMiddleware";
export { default as compose } from "./compose";
Copy the code
We also need to write some utility functions for us to use
// utils.js
// Check whether an object is a Plain object
export const isPlainObject = (obj) = > {
if( obj === undefined || Object.getPrototypeOf(obj) ! = =Object.prototype ) return false;
return true;
}
// To generate a random string
export const getRandomStr = (length) = > {
if(typeoflength ! = ="number") length = 6;
return Math.random().toString(36).substr(2, length).split("").join(".");
}
// In redux, redux will call the reducer at a specific time to verify whether the reducer meets the requirements at all times
The reducer we passed in only triggered actions with special type values, so we reduced those special types
// Values are put into the utilTypes object for subsequent normal use
export const utilTypes = {
INIT: () = > ({
type: `@@redux/INIT${getRandomStr()}`
}),
UNKOWN: () = > ({
type: `@@redux/UNKOWN${getRandomStr()}`})}Copy the code
Handwritten createStore
To write about these principles, we can start with a few phenomena. We can start by looking at what feature points are attached to createStore that we need to implement:
-
CreateStore can accept three parameters:
- Reducer: As the first parameter, this parameter is also reducer
createStore
Is a function that is passed in to handle the data warehouse according to action - DefaultState: The second parameter, optionally left blank, is meant to represent the default value for the entire data warehouse
- Enhancer: progressively enhanced middleware application function configured to change the behavior of the entire Redux repository Disptach based on the middleware being passed
- Reducer: As the first parameter, this parameter is also reducer
-
CreateStore returns an object with the following structure:
- Dispatch: a function used to modify data in the current Redux data warehouse. After Dispatch distributes an action, the Reducer is executed. The Reducer modifies data in the warehouse according to the corresponding action
- Subcribe: Callback to execute each time the value of the repository changes to subscribe
- ReplaceReducer: Very simple, used to replace the current reducer function
- GetState: To get the current state of the Redux repository
- Symbol(“observerable”): This is related to ESNext and is used to generate a publishing-subscribe method. (This blog post will not explore it because we almost never use it, but its implementation principle is still complex and was preset by Redux for future scenarios.)
-
While in the inventory, Redux automatically triggers a special action to ensure that your incoming Reducer is compliant with the Reducer criteria
OK, knowing the createStore’s form of existence and its function, we can implement it bit by bit according to our own logical thinking
// Introduce two utility functions that will be used when coding
import { utilTypes, isPlainObject } from "./utils";
// The createStore function
export default (reducer, defaultState, enhancer) => {
// First reducer is mandatory. If there is an error, it cannot be tolerated
if(typeofreducer ! = ="function") throw new TypeError("Expected the reducer to be a function");
// We know that we sometimes do not pass defaultState, but write the second argument as enhancer
// Redux can also recognize it, so we can do a simple check (of course redux does the check in more detail)
if( typeof defaultState === "function" && enhancer === undefined ) enhancer = defaultState;
// Why do we want to save these two guys internally
// These things can be replaced or modified, which might be a little more intuitive to write
let curReducer = reducer; // Current reducer
let curState = defaultState; // The current status
let listeners = []; // An array of listener callback functions to store later
if( typeof enhancer === "function" ) {
// There is some middleware action to be done, which is related to the subsequent applyMiddleware
return;
}
// The user has not passed enhancer, so we will follow the basic flow
CreateStore can return an object with several functions in it
const dispatch = () = > {};
const replaceReducer = () = > {};
const subcribe = () = > {};
const getState = () = > {};
return {
dispatch,
replaceReducer,
subcribe,
getState
}
}
Copy the code
At this point, we’ll test the createStore function ourselves. I’m sure it will give you an object that looks like the official Redux (just missing Symbol(observerble), but we won’t write that).
So what we’re going to do is, step by step, we’re going to refine each of these functions, and we’re going to take care of the little details
Dispatch function and getState, replaceReducer implementation
As we know, the function of Dispatch is very simple. It accepts an action as a parameter, and then internally triggers the reducer and passes the action to the Reducer to modify the values in the repository
// createStore.js.const dispatch = () = > {
// dispatch the first step is to determine the legitimacy of the action:
// 1. Must be a flat Object: prototype points to Object.prototype
// 2. Must contain a type attribute
if( !isPlainObject(action) || action["type"= = =undefined ) {
// Indicates that the verification fails
throw new TypeError("action must be a plain object with property 'type'");
}
console.warn("curState", curState);
// After validation, we need to reduce the reducer
curState = curReducer(curState, action);
// We know that after we have changed the status, we need to do one more thing, which is to record all the listeners in the process
// The callbacks are executed in sequence
if( listeners.length === 0 ) return;
listeners.forEach(cb= >cb()); }; .const replaceReducer = (reducer) = > {
// replaceReducer is also very simple
// We just need to do a check on reducer before assignment
if(reducer(undefined, utilTypes.INIT) === undefined) return;
curReducer = reducer;
};
const getState = () = > {
// Add getState as well, because it is really easy
returncurState; }; .return {
dispatch,
replaceReducer,
subcribe,
getState
}
...
Copy the code
At this point we said that redux validates the reducer at the beginning and automatically triggers an action, so we often use this to initialize, but remember that once the default value is used in the createStore, the single Reducer default value is directly invalid
// createStore.js.if( typeof enhancer === "function" ) {
// There is some middleware action to be done, which is related to the subsequent applyMiddleware
return;
}
// We need to add some code to test the reducer's validity
const result = curReducer(defaultState, utilTypes.INIT); // Trigger an init type
if( result === undefined ) {
throw new TypeError("reducer cannot return undefined"); }...Copy the code
Ok, at this time our dispatch function is finished, after testing, it can also achieve the desired function
Subcribe function implementation
Subcribe is primarily used to pass listener functions that will be run when the state changes
.const subcribe = (listener) = > {
if(typeoflistener ! = ="function") return;
listeners.push(listener);
// Subcribe also returns a function for touch listening
return () = > {
let index = listeners.findIndex(listener);
if( index === -1 ) return;
listeners.splice(index, 1); }}...Copy the code
Handwritten bindActionCreator
BindActionCreator is also relatively simple. Its main functions are as follows:
-
BindActionCreator takes two arguments
- ActionCreator/actionCreatorSet: An action creator function, or a collection of objects that create action functions
- Dispatch: The corresponding warehouse Dispatch function
-
BindActionCreator will return a corresponding function or set of functions directly, and when we call this function, it will automatically dispatch, so we don’t need to do it manually
In fact, this function is quite simple. It is just a set of higher-order functions. You give me a function, I give you a new function, and the new function automatically calls dispatch for you
// bindActionCreator.js
// To help us create an action creator that can dispatch automatically
const getAutoDispatchActionCreator = (actionCreator, dispatch) = > (. args) = >dispatch(actionCreator(... args));export default (actionCreator, dispatch) => {
console.log(actionCreator);
// Let's do a compatibility first
if(! (typeofactionCreator ! = ="function" || typeofactionCreator ! = ="object")) {
throw new TypeError("bindActionCreators expected an object or a function");
}
// If dispatch does not transmit, we simply return the source object
if(typeofdispatch ! = ="function") return actionCreator;
// If actionCreator is a function, not an object, we call it directly
if(typeof actionCreator === "function") {
return getAutoDispatchActionCreator(actionCreator, dispatch);
}
const autoDispatchCreators = {};
// If actionCreator is an object, we need to return a new object, and each function in the object is mapped to a new function
for(const prop in actionCreator) {
if(typeofactionCreator[prop] ! = ="function") {
throw new TypeError("bindActionCreators expected an object or a function");
}
autoDispatchCreators[prop] = getAutoDispatchActionCreator(actionCreator[prop]);
}
return autoDispatchCreators;
}
Copy the code
Handwritten combineReducers
CombineReducer we want to appear his two phenomena:
- Take a function object as a parameter, or a single function as a parameter (although this doesn’t make much sense)
- A reducer function is returned, which can accept actions to change the state
CombineReducers, in fact, is a reducer aggregation, we know that a store can only have a reducer, this reducer is a function, the function should finally return a new state, So how do we aggregate this number of reducers into one final Reducer?
// combineReducers.js
import { utilTypes } from "./utils";
export default (reducers) => {
// If the passed reducers is a function, return it directly
if( typeof reducers === "function" ) return reducers;
// Check reducers
const (key in reducers) {
let curReducer = reducers[key]; // Get the current reducer
// When the reducer is received, confirm the legitimacy of the reducer
// Redux validates redux twice
let passFlag = true;
const arr = [utilTypes.INIT, utilTypes.UNKOWN];
arr.forEach(el= > {
let result = curReducer(el());
if(result === undefined) passFlag = false;
})
if( passFlag === false ) {
// Indicates that the verification fails
throw new TypeError("reducer cannot return undefined"); }}// What I need to do is return a reducer function that I passed to the store
return (state = {}, action) = > {
const lastState = {};
// When I got an action, what I had to do was very simple. I directly went through the reducers that were uploaded
for( const key in reducers ) {
< reducer > < reducer > < reducer
// The state that belongs to him is passed in instead of the entire state
{loginInfo: {}, userInfo: {}};
// We reducers[key] can get the reducer of login, at this time what we need to pass is
/ / loginInfo reducer
lastState[key] = reducers[key](state[key], action);
}
// The lastReducers are the state objects that have already been processed
// The action is an object.
The reducer does not have the type value of this action
// Then it will return the default value, if it has, then it will return the corresponding execution result
// So I let each reducer go once, so that we can achieve an aggregation effect, if any
// If not, the default value will be returned
return lastState
}
}
Copy the code
Handwritten applyMiddleware
This is the most important one. ApplyMiddleware is one of the more complicated parts of redux’s source code. Make sure you understand how redux middleware works (the Onion model) or you won’t be able to get around it. You also need to know a little bit about composing functions
Before writing applyMiddleware, we need to do a little bit of preparation. Instead of leaving a compose file, let’s get it out of the way
The compose file exports a method that provides the following basic functionality:
- Accept multiple functions as arguments
- Returns a new function that combines the functions of all previous functions and returns the final result
// compose.js
const compose = (. funcs) = > {
if(! funcs || ! funcsinstanceof Array) return (. args) = > args;
return (. args) = > funcs.reduce((fst, sec) = >fst(sec(... arg)); }Compose compose compose compose compose compose compose compose compose compose compose
function add(n) {
return n + 2;
}
function mult(n) {
return n * n;
}
const func = compose(add, mult);
const result = func(3);
console.log("result", result); // The output will be 11. It will first give 3 to mult and then give the result of mult to add
export default compose;
Copy the code
Ok, so with that in mind, here’s what applyMiddleware does:
- Receives an unlimited number of middleware handlers (which return a dispatch creation function) as arguments,
- Return a new function, enhancer, which takes a createStore function as an argument, and returns a function that actually initializes the repository when the enhancer function completes execution
- Inside the initialization warehouse function, the createstore method is executed to get store, and the store dispatch method is modified by calling middleware. After all middleware are registered, the processed Dispatch is returned
Note again: since this is the Principles blog, I won’t go into much detail about the specification of redux middleware and the whole concept of redux’s middleware model, which must be mastered otherwise you won’t know what I’m talking about
// appliMiddleware.js
import compose from "./compose";
export default(... middlewares) => {return function enhanced(createStore) {
return (reducer, defaultState) = > {
const store = createStore(reducer, defaultState);
// This dispatch is the function we will eventually pass to override the store's dispatch
// Why override: because we need to iterate on the middleware wrapper enhancement of the dispatch function
// It doesn't matter what the function is in the first place, you could just write it as an empty function, or you could equal store.dispatch
// The error function is called
let dispatch = () = > throw new Error("u can't use dispatch now");
// Because we know that each middleware handler returns an array of functions created with dispatch after execution
// What is the dispatch creation function
// So we can get the dispatches of all current middleware handler functions to create functions
const dispatchProducers = middlewares.map(el= > el(store));
// At this point we are going to compose, and we just need to get the result of the last dispatch creation function
// The last dispatch creation function creates the multiple incremented dispatch we want
// A final dispatch creation function will be returned when we go through the compose combination, and then we will
// store. Dispatch creates a function to the dispatch as an argument, which generates a dispatch for us
dispatch = compose(dispatchProducers)(store.dispatch);
return {
...store,
dispatch
}
}
}
}
Copy the code
Now that we’re done with applyMiddleware, but for those of you who don’t understand it, what does the middleware specification look like
const middleware = (store) = > (next) = > (action) = > {
// Store: is the store of the whole warehouse. The dispatch in this store is the original dispatch
// So when we use the map above, we need to pass in store. The reason why we do this is because
// Suppose you are a bully and don't want to enhance someone else's already enhanced dispatch at all
// The original dispatch(i.e. Store.dispatch), which is designed to give developers more flexibility
// Next: This next is actually a dispatch after being processed by the previous middleware. Why is it called Next
// If you think about it, the middleware compose combination starts from the last package, but is executed from the first
// A (b(args)), you say that the next b is not a, but the execution must be a first, he means so
When appropriate, you can hand off your action to the next middleware to wrap it up. When next doesn't, next will be store.dispatch
next(action);
}
Copy the code
The middleware model must be clear, otherwise you won’t be able to read it
OK, we left a tail behind and we need to handle the case where enhancer was passed in createStore
// createStore.if( typeof enhancer === "function" ) {
return enhancer(createStore)(reducer, defaultState); // The first parameter is createStore, and the second parameter is Reducer and defaultState}...Copy the code
Now that we’ve covered all four of Redux’s core features, there are a few more, but they are all in the corner, so I won’t mention them here. I hope this blog post has been helpful and I hope you can get some advice. See you~