In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-03-26 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article mainly explains "how to handwrite redux from scratch in react.js". The content in the article is simple and clear, and it is easy to learn and understand. Please follow the editor's train of thought to study and learn how to handwrite redux from scratch in react.js.
1. What is Redux?
Redux is a JavaScript state container that provides predictable state management. Redux supports other interface libraries in addition to being used with React. Redux is small and sturdy, with only 2KB. Here we need to be clear: there is no strong binding relationship between Redux and React. The purpose of this article is to understand and implement a Redux, but it will not cover react-redux (just understand one point of knowledge at a time, and react-redux will appear in the next article).
two。 Implement a Redux from scratch
Let's forget the concept of Redux, start with an example, and create a project using create-react-app: toredux.
Code directory: in myredux/to-redux.
Modify the body in public/index.html to the following:
Hello, everyone in the front-end universe. I am Liu Xiaoxi Blue Pink, the author of the front-end universe.
The function we want to implement is shown in the above figure, which can change the color of the font for the entire application when the button is clicked.
Modify the src/index.js as follows (code: to-redux/src/index1.js):
Let state = {color: 'blue'} / / render application function renderApp () {renderHeader (); renderContent ();} / render title part function renderHeader () {const header = document.getElementById (' header'); header.style.color = state.color;} / / render content part function renderContent () {const content = document.getElementById ('content') Content.style.color = state.color;} renderApp (); / / Click the button to change the font color document.getElementById ('to-blue'). Onclick = function () {state.color =' rgb (0,51,254)'; renderApp ();} document.getElementById ('to-pink'). Onclick = function () {state.color =' rgb (247109,132)'; renderApp ();}
This application is very simple, but it has a problem: state is a shared state, but anyone can modify it. Once we modify this state at will, it can lead to errors. For example, in renderHeader, setting state = {} can easily cause unexpected errors.
However, many times, we do need to share the state, so we can consider setting some thresholds, for example, we agreed that the global state cannot be modified directly, but must be modified through a certain way. For this reason, we define a changeState function, which is responsible for modifying the global state.
/ / continue to append the code function changeState (action) {switch (action.type) {case 'CHANGE_COLOR': return {... state, color: action.color} default: return state;} in index.js
We agree that the state can only be modified through changeState, which accepts a parameter action, a normal object containing the type field, and the type field is used to identify your type of operation (that is, how to modify the state).
We want to click the button to change the font color of the entire application.
/ / continue to append the code document.getElementById ('to-blue'). Onclick = function () {let state = changeState ({type:' CHANGE_COLOR', color: 'rgb (0,51,254)'}) in index.js; / / after the status modification, you need to re-render the page renderApp (state) } document.getElementById ('to-pink'). Onclick = function () {let state = changeState ({type:' CHANGE_COLOR', color: 'rgb (247,109,132)'}); renderApp (state);}
Pull away from store
Although we have agreed on how to modify the state, state is a global variable, and we can easily modify it, so we can consider turning it into a local variable and defining it inside a function (createStore), but we also need to use state externally, so we need to provide a method getState () so that we can get state in createStore.
Function createStore (state) {const getState = () = > state; return {getState}}
Now, we can get the state through the store.getState () method. (to be clear here, state is usually an object, so this object can actually be modified directly externally, but if the deep copy state returns, then it cannot be modified externally, since the redux source code returns state directly, we do not make a deep copy here, which consumes performance.)
Just getting the state is far from enough, we also need a way to modify the state, now that the state is a private variable, we must also put the method to modify the state in the createStore and expose it to external use.
Function createStore (state) {const getState = () = > state; const changeState = () = > {/ /... code} return in changeState {getState, changeState}}
Now the code in index.js looks like this (to-redux/src/index2.js):
Function createStore () {let state = {color: 'blue'} const getState = () = > state Function changeState (action) {switch (action.type) {case 'CHANGE_COLOR': state = {... state, color: action.color} return state; default: return state } return {getState, changeState}} function renderApp (state) {renderHeader (state); renderContent (state);} function renderHeader (state) {const header = document.getElementById ('header'); header.style.color = state.color;} function renderContent (state) {const content = document.getElementById (' content') Content.style.color = state.color;} document.getElementById ('to-blue'). Onclick = function () {store.changeState ({type:' CHANGE_COLOR', color: 'rgb (0,51,254)'}); renderApp (store.getState ()) } document.getElementById ('to-pink'). Onclick = function () {store.changeState ({type:' CHANGE_COLOR', color: 'rgb (247,109,132)'}); renderApp (store.getState ());} const store = createStore (); renderApp (store.getState ())
Although we are now pulling away from the createStore method, it is clear that this method is not universal at all, and both the state and changeState methods are defined in createStore. In this case, other applications cannot reuse this mode.
The logic of changeState should be defined externally, because the logic of modifying the state of each application must be different. We stripped this part of the logic out and renamed it reducer (asking why it is called reducer, just to be consistent with redux). What does reducer do? to put it bluntly, it is to calculate the new state according to the type of action. Because it is not defined within the createStore, there is no direct access to the state, so we need to pass the current state to it as a parameter. It is as follows:
Function reducer (state, action) {switch (action.type) {case 'CHANGE_COLOR': return {... state, color: action.color} default: return state;}}
CreateStore Evolutionary Edition
Function createStore (reducer) {let state = {color: 'blue'} const getState = () = > state; / / rename the changeState here to `Secretch` const dispatch = (action) = > {/ / reducer receives the old state and action, and returns a new state state = reducer (state, action);} return {getState, dispatch}}
The state of different applications must be different, and it must be unreasonable for us to define the value of state within the createStore.
Function createStore (reducer) {let state; const getState = () = > state; const dispatch = (action) = > {/ / reducer (state, action) returns a new state state = reducer (state, action);} return {getState, dispatch}}
Note the definition of reducer, which returns directly to the old state when it encounters an unrecognized action, and now we use this to return to the initial state.
If you want state to have an initial state, it is actually very simple. Let's take the initialization value of the initial state as the default value of the parameter of reducer, and then dispatch an action in createStore that reducer does not understand. In this way, the default value of the state can be obtained when getState is called for the first time.
CreateStore Evolutionary Edition 2.0
Function createStore (reducer) {let state; const getState = () = > state; / / whenever `reducer` is an action, we need to call `reducer` to return a new state const dispatch = (action) = > {/ / reducer (state, action) return a new state state = reducer (state, action) } / / if you have a type of action whose value is `@ @ redux/__INIT__$ {Math.random ()}`, I respect you as a ruthless dispatch ({type: `@ @ redux/__INIT__$ {Math.random ()}`); return {getState, dispatch}}
Now this createStore can be used everywhere, but do you think it is stupid to manually renderApp () after every dispatch? in the current application, it is called twice. If you need to modify state 1000 times, do you call renderApp () 1000 times manually?
Can you simplify it? RenderApp () is called automatically every time the data changes. Of course, it is impossible to write renderApp () in the dispatch of createStore (), because in other applications, the function name may not be called renderApp (), and it may not only trigger renderApp (). The publish-subscribe model can be introduced here to notify all subscribers when the status changes.
CreateStore Evolutionary Edition 3.0
Function createStore (reducer) {let state; let listeners = []; const getState = () = > state; / / subscribe returns an unsubscribe method const subscribe = (ln) = > {listeners.push (ln); / / unsubscribe is also allowed after subscription. / / is it true that after I subscribe to a magazine, I am not allowed to unsubscribe? Terrible ~ const unsubscribe = () = > {listenerslisteners = listeners.filter (listener = > ln! = = listener);} return unsubscribe;}; const dispatch = (action) = > {/ / reducer (state, action) returns a new state state = reducer (state, action); listeners.forEach (ln = > ln ()) } / / if you have an action whose type value is exactly equal to `@ @ redux/__INIT__$ {Math.random ()}`, I respect you as a ruthless dispatch ({type: `@ redux/__INIT__$ {Math.random ()}`); return {getState, dispatch, subscribe}}
At this point, the simplest redux has been created, and createStore is the core of redux. Let's rewrite our code with this stripped-down version of redux. The contents of the index.js file are updated as follows (to-redux/src/index.js):
Function createStore () {/ / code (copy the above createStore code here by yourself)} const initialState = {color: 'blue'} function reducer (state = initialState, action) {switch (action.type) {case' CHANGE_COLOR': return {... state Color: action.color} default: return state } const store = createStore (reducer); function renderApp (state) {renderHeader (state); renderContent (state);} function renderHeader (state) {const header = document.getElementById ('header'); header.style.color = state.color;} function renderContent (state) {const content = document.getElementById (' content'); content.style.color = state.color } document.getElementById ('to-blue'). Onclick = function () {store.dispatch ({type:' CHANGE_COLOR', color: 'rgb (0,51,254)'}) } document.getElementById ('to-pink'). Onclick = function () {store.dispatch ({type:' CHANGE_COLOR', color: 'rgb (247,109,132)'});} renderApp (store.getState ()); / / re-render store.subscribe (() = > renderApp (store.getState ()) every time the state changes)
If we now hope that the font color is not allowed to be changed after clicking on Pink, we can also unsubscribe:
Const unsub = store.subscribe (() = > renderApp (store.getState (); document.getElementById ('to-pink'). Onclick = function () {/ / code... Unsub (); / / Unsubscribe}
By the way: reducer is a pure function (consult the data yourself if you don't know the concept of pure function), it takes in the previous state and action, and returns the new state. Don't ask why there must be a type field in action, it's just a convention (that's how redux is designed)
The remaining question: why does reducer have to return a new state instead of directly modifying the state? You are welcome to leave your answer in the comments section.
Now that we've worked out the core code of redux step by step, let's review the design ideas of redux:
Redux design idea
Redux stores the entire application state (state) in one place (usually called store)
When we need to change the state, we must dispatch an action (action is an object with a type field)
The special state handler reducer receives the old state and action and returns a new state
Subscriptions are set up through subscribe, notifying all subscribers each time an action is dispatched.
We already have a basic version of redux, but it doesn't meet our needs yet. Our usual business development is not as simple as the example written above, so there is a problem: the reducer function may be very long because there are so many types of action. This is definitely not conducive to code writing and reading.
Imagine that there are 100 kinds of action in your business that need to be dealt with. Writing these 100 situations in a reducer is not only disgusting, but also colleagues who maintain the code later.
Therefore, it is best to write reducer separately and then merge the reducer. Please welcome our combineReducers (which is the same as the name of the redux library) to make its debut.
CombineReducers
First of all, we need to be clear: combineReducers is just a utility function, which, as we said earlier, merges multiple reducer into a single reducer. CombineReducers returns reducer, which means it is a higher-order function.
Let's take an example to illustrate that although redux doesn't have to work with react, since it works best with react, here, take the react code as an example:
This time in addition to the above display, we have added a counter function (using React refactoring = > to-redux2):
/ / now our state structure is as follows: let state = {theme: {color: 'blue'}, counter: {number: 0}}
Obviously, modifying themes and counters can be separated, and it is a better choice to have different reducer to deal with.
Store/reducers/counter.js
The state that handles the counter.
Import {INCRENENT, DECREMENT} from'.. / action-types' Export default counter (state = {number: 0}, action) {switch (action.type) {case INCRENENT: return {... state, number: state.number + action.number} case DECREMENT: return {... state Number: state.number-action.number} default: return state }} store/reducers/theme.js
Responsible for handling the state that modifies the theme color.
Import {CHANGE_COLOR} from'.. / action-types'; export default function theme (state = {color: 'blue'}, action) {switch (action.type) {case CHANGE_COLOR: return {... state, color: action.color} default: return state;}}
Each reducer is responsible for managing only the portion of the global state that it is responsible for. The state parameters of each reducer are different, corresponding to the part of state data it manages.
Import counter from'. / counter'; import theme from'. / theme'; export default function appReducer (state= {}, action) {return {theme: theme (state.theme, action), counter: counter (state.counter, action)}}
AppReducer is the merged reducer, but when there is more reducer, it is also tedious to write this, so we write a tool function to generate such an appReducer, and we name this tool function combineReducers.
Let's try to write the utility function combineReducers:
Train of thought:
CombineReducers returns reducer
The input parameter of combineReducers is an object composed of multiple reducer
Each reducer only deals with the part of the global state that it is responsible for.
/ / reducers is an object, and the attribute value is each split reducer export default function combineReducers (reducers) {return function combination (state= {}, action) {/ / reducer. The return value is the new state let newState = {}; for (var key in reducers) {newState [key] = reducers [key] (state [key], action);} return newState;}}
The child reducer will be responsible for returning the default value of state. For example, in this case, dispatch ({type:@@redux/__INIT__$ {Math.random ()}}) is in createStore, and the function combination returned by combineReducers (reducers) is passed to createStore.
The initial values of newState.theme and newState.counter are returned according to the two child reducer of state=reducer (state,action), newState.theme=theme (undefined, action), newState.counter=counter (undefined, action), counter and theme, respectively.
Using this combineReducers, you can rewrite store/reducers/index.js
Import counter from'. / counter'; import theme from'. / theme'; import {combineReducers} from'.. / redux'; / / obviously a lot of ~ export default combineReducers ({counter, theme})
Although the combineReducers we wrote seems to meet our needs, it has the disadvantage that a new state object is returned each time, which leads to meaningless re-rendering when the data remains unchanged. So we can judge the data and return the original state when the data has not changed.
CombineReducers Evolutionary Edition
Some judgments are omitted in the code. The parameters passed by default meet the requirements. If you are interested, you can check the source code to determine and deal with the validity of parameters. Export default function combineReducers (reducers) {return function combination (state= {}, action) {let nextState = {}; let hasChanged = false / / whether the status changes for (let key in reducers) {const previousStateForKey = state [key]; const nextStateForKey = reducers [key] (previousStateForKey, action); nextState [key] = nextStateForKey; / / only if all nextStateForKey are equal to previousStateForKey, the hasChanged value is false hasChangedhasChanged = hasChanged | | nextStateForKey! = = previousStateForKey If} / / state has not changed, return the original object return hasChanged? NextState: state;}}
ApplyMiddleware
In the official document, the explanation of applyMiddleware is very clear, and the following content also refers to the contents of the official document:
Log record
Consider a small problem. What do we do if we want to print state on the console before each state change?
The simplest thing is:
/ /. {console.log (store.getState ()); store.dispatch (actions.add (2));}} > + / /.
Of course, this approach is certainly not advisable. If we distribute it 100 times in our code, we can't write it 100 times. Since the state is printed when the state changes, that is, the state is printed before the dispatch, we can override the store.dispatch method and print the state before dispatch.
The command let store = createStore (reducer); const next = store.dispatch; / / next is to be consistent with the source code of the middleware store.dispatch = action = > {console.log (store.getState ()); next (action);}
Crash message
Suppose we need not only to print the state, but also to print out the error message when there is an error in the dispatch exception.
Const next = store.dispatch; / / next is named to be consistent with the source code of middleware store.dispatch = action = > {try {console.log (store.getState ()); next (action);} catct (err) {console.error (err);}}
If we have other requirements, then we need to constantly modify the store.dispatch method, which makes this part of the code difficult to maintain.
So we need to separate loggerMiddleware from exceptionMiddleware.
Let store = createStore (reducer); const next = store.dispatch; / / next is named to be consistent with the source code of middleware const loggerMiddleware = action = > {console.log (store.getState ()); next (action);} const exceptionMiddleware = action = > {try {loggerMiddleware (action);} catch (err) {console.error (err);}} store.dispatch = exceptionMiddleware
We know that many middleware are provided by third parties, so store must need to be passed to middleware as a parameter to further rewrite:
Const loggerMiddleware = store = > action = > {const next = store.dispatch; console.log (store.getState ()); next (action);} const exceptionMiddleware = store = > action = > {try {loggerMiddleware (store) (action);} catch (err) {console.error (err);}} / / use store.dispatch = exceptionMiddleware (store) (action)
Now there is a small problem. The loggerMiddleware in exceptionMiddleware is written dead, which is definitely unreasonable. We hope that this is a parameter so that it can be used flexibly. It makes no sense that only exceptionMiddleware needs flexibility. Regardless of loggerMiddleware, it is further rewritten as follows:
Const loggerMiddleware = store = > next = > action = > {console.log (store.getState ()); return next (action);} const exceptionMiddleware = store = > next = > action = > {try {return next (action);} catch (err) {console.error (err);}} / / use const next = store.dispatch; const logger = loggerMiddleware (store); store.dispatch = exceptionMiddleware (store) (logger (next))
Now, we have a general middleware writing format.
Middleware receives a dispatch function of next () and returns a dispatch function, which will be used as the next () of the next middleware.
But there is a small problem, when there are a lot of middleware, the code to use middleware will become very cumbersome. To do this, redux provides a utility function for applyMiddleware.
As we can see above, what we finally want to change is dispatch, so we need to rewrite store and return the store after modifying the dispatch method.
Therefore, we can make the following points clear:
The return value of applyMiddleware is store
ApplyMiddleware must accept middleware as a parameter
ApplyMiddleware should accept store as the input parameter, but the input parameter in the redux source code is the input parameter of createStore and createStore. Think about it, there is no need to create a store externally. After all, the store created externally has no other function except as a parameter. It is better to pass in the parameters that createStore and createStore need to use.
/ / applyMiddleWare returns store. Const applyMiddleware = middleware = > createStore = > (... args) = > {let store = createStore (.. ARGs); let middle = loggerMiddleware (store); let dispatch = middle (store.dispatch); / / New dispatch method / / returns a new store--- overriding the dispatch method return {... store, dispatch}}
The above is the case of one middleware, but we know that middleware may be one or more, and we mainly need to solve the problem of multiple middleware and further rewrite it.
/ / applyMiddleware returns store. Const applyMiddleware = (... middlewares) = > createStore = > (... args) = > {let store = createStore (.. ARGs); / / incidentally: instead of passing store directly in the redux source code, getState and dispatch are passed to middleware let middles = middlewares.map (middleware = > middleware (store)). / / now we have multiple middleware, and we need to enhance dispatch let dispatch = middles.reduceRight ((prev, current) = > prev (current), store.dispatch); return {. Store, dispatch}}
I don't know if you understand the middles.reduceRight above. Here's a detailed explanation:
/ * three middleware * / let logger1 = store = > dispatch = > action = > {console.log ('111'); dispatch (action); console.log (' 444');} let logger2 = store = > dispatch = > action = > {console.log ('222'); dispatch (action); console.log (' 555')} let logger3 = store = > dispatch = > action = > {console.log ('333'); dispatch (action) Console.log ('666');} let middle1 = logger1 (store); let middle2 = logger2 (store); let middle3 = logger3 (store); / / applyMiddleware (logger1,logger3,logger3) (createStore) (reducer) / / if store.dispatch = middle1 (middle2 (middle3 (store.dispatch)) is directly replaced)
Observe the above middle1 (middle2 (middle3 (store.dispatch), if we regard middle1,middle2,middle3 as every item in the array, if you are familiar with the API of the array, you can think of reduce, if you are not familiar with reduce, you can check the MDN document.
/ / applyMiddleware (logger1,logger3,logger3) (createStore) (reducer) / / reduceRight execute middles.reduceRight ((prev, current) = > current (prev), store.dispatch) from right to left; / / first prev: store.dispatch current: middle3 / / second prev: middle3 (store.dispatch) current: middle2 / / third prev: middle2 (middle3 (store.dispatch) current: middle1 / / result middle1 (middle2 (middle3 (store.dispatch)
Students who have read the source code of redux may know that a compose function is provided in the source code, while the compose function does not use reduceRight, but uses reduce, so the code is slightly different. But the analysis process is still the same.
Compose.js
Export default function compose (... funcs) {/ / if there is no middleware if (funcs.length = 0) {return arg = > arg} / / the middleware length is 1 if (funcs.length = 1) {return funcs [0]} return funcs.reduce ((prev, current) = > (. Args) = > prev (current (. ARGs));}
With regard to the writing of reduce, it is suggested that an analysis should be carried out like the reduceRight above.
Use the compose utility function to rewrite applyMiddleware.
Const applyMiddleware = (... middlewares) = > createStore = > (... args) = > {let store = createStore (.args); let middles = middlewares.map (middleware = > middleware (store)); let dispatch = compose (. Middles) (store.dispatch); return {. Store, dispatch}}
BindActionCreators
Redux also provides us with the bindActionCreators utility function, which is so simple that we rarely use it directly in the code, which is used in react-redux. Here, a brief explanation:
/ / usually we would write our actionCreator import {INCRENENT, DECREMENT} from'.. / action-types' like this Const counter = {add (number) {return {type: INCRENENT, number}}, minus (number) {return {type: DECREMENT, number} export default counter
When distributing, we need to write:
Import counter from 'xx/xx'; import store from' xx/xx'; store.dispatch (counter.add ())
Of course, we can also write our actionCreator as follows:
Function add (number) {return {type: INCRENENT, number}}
When you dispatch, you need to write:
Store.dispatch (add (number))
The above code has one thing in common, that is, store.dispatch dispatches an action. So we can consider writing a function that binds store.dispatch and actionCreator.
Function bindActionCreator (actionCreator, dispatch) {return (... args) = > dispatch (actionCreator (.args));} function bindActionCreators (actionCreator, dispatch) {/ / actionCreators can be an ordinary function or an object if (typeof actionCreator = 'function') {/ / if it is a function, the return value bindActionCreator (actionCreator, dispatch) of dispatch function when called } else if (typeof actionCreator = = 'object') {/ / if it is an object, each item of the object must return bindActionCreator const boundActionCreators = {} for (let key in actionCreator) {boundActionCreators [key] = bindActionCreator (actionCreator [key], dispatch);} return boundActionCreators;}}
When in use:
Let counter = bindActionCreators (counter, store.dispatch); / / Distribution counter.add (); counter.minus (); Thank you for reading, above is the content of "how to handwrite redux from scratch in react.js". After the study of this article, I believe you have a deeper understanding of how to handwrite redux from scratch in react.js, and the specific use needs to be verified in practice. Here is, the editor will push for you more related knowledge points of the article, welcome to follow!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.