Before the introduction
Hello friends, I am Jarvis from the Push ah front end team. This time I will share the content of “interpretation of Redux, React-Redux source code”. If you have a different opinion, please comment on it in the comments section ~ πππ
Writing in the front
As React’s original “state management repository,” Redux has always been a controversial presence, with some people loving its advantages of a single data flow and easy management of backtracking, while others hate the complexity of its introductory demo. But overall, Redux is still an impressively smart JS library. As a deep user of React, I have a lot to share.
After experiencing Redux for the first time, I had the following questions in mind:
reducer
theswitch
Why are statements writtendefault
Branches, what’s the problem if I don’t write them? π€const counter = (state, action) = > { switch (action.type) { case 'ADD': return state + 1; // I don't want to write // default: // return state;}}Copy the code
- Redux wasn’t enough? Why the React-Redux? π§
- Components don’t write
connect
.store
updateNot triggerComponent updates, according to? π€¨ - Why use
Vuex
When the amount of code is so small, and useRedux
So much code to write? π€¨ - I want to
action
With asynchronous methods, why use middleware? π€¨ - Middleware is a hammer? π€¨
How do you read Redux? Why did someone teach me durex[‘ DJ ΚΙreks]π€£
Maybe you’re easy to talk about, or maybe you’re confused too. In fact, for this kind of problem, the most fundamental solution is to understand the principle, can clear the fog and see the light.
Next we will combine the source code, Redux and React-Redux principle to explore π§π§π§.
Relationship between the two
Before we talk about the source code, let’s talk about the relationship between the two,
Redux is a js based data warehouse that defines actions when data changes through manual subscription updates.
If there were no React-redux, we would use Redux in this situation:
import { createStore } from 'redux';
// 1. Create the Reducer
function counter(state, action) {
switch (action.type) {
case 'ADD':
return state + 1;
default:
returnstate; }}// 2. Create a store
const store = Redux.createStore(counter);
// 3. Use in application
function App() {
const [, forceUpdate] = useState({});
useEffect(() = > {
const unsubscribe = store.subscribe(() = > {
forceUpdate({});
});
return () = >unsubscribe(); } []);const add = () = > {
store.dispatch({
type: 'ADD'}); };return <div onClick={add}>{store.getState()}</div>;
}
Copy the code
Huh? Even manually subscribe to update, is it very troublesome π€ ~
We want to simplify the code, after all, writing subscription updates in components is too intrusive for our business, so we found React-Redux. React-redux is the bridge between React and Redux, which simplifies the cost of using Redux in React. Make it easy for us to get the store we want in the React component and automatically subscribe to updates, so our actions will be simple:
import { createStore } from 'redux';
// 1. Create the Reducer
function counter(state, action) {
switch (action.type) {
case 'ADD':
return state + 1;
default:
returnstate; }}// 2. Create a store
const store = Redux.createStore(counter);
// 3. Share the status in the root application
ReactDom.render(
<Provider store={store}>
<App />
</Provider>.document.getElementById('root'))// 4. Use in business components
const App = connect()((props) = > {
const add = () = > { props.dispatch({ type: 'ADD'}); };return <div onClick={add}>{store.getState()}</div>;
})
Copy the code
As you can see, we no longer need to subscribe to updates manually, and components processed through the connect method now have new props. This is actually the core part of react-Redux.
Redux is not a data warehouse specifically designed for React. It can be used in any JS library.
1. Redux
1.1. Know in advance
- Functional programming
- Pure functions
- Compose function
- Currie,
- The onion model
1.2. Source code (1) : createStore
1.2.1. createStore
-
role
- Creating a data warehouse
store
- Creating a data warehouse
-
The core source
// Some code is omitted export default function createStore(reducer, preloadedState, enhancer) { // Handle the edge case./ / middleware if (typeofenhancer ! = ='undefined') { return enhancer(createStore)(reducer, preloadedState) } // Save the Reducer, because there may be overwriting operations let currentReducer = reducer; // Store the current state let currentState = preloadedState; // Store the subscription function let currentListeners; // Store the next batch of subscription functions let nextListeners = currentListeners; // Whether the Reducer is being executed let isDispatching = false; // Deep copy currentListeners to nextListeners function ensureCanMutateNextListeners() {} // Get the current state function getState() :S {} // Add a subscription function function subscribe(listener: () => void) {} // Call Reducer to generate new state function dispatch(action: A) {} / / replace reducer function replaceReducer(nextReducer); // Extensible methods for observer pattern/responsive frameworks, such as Vue function observable() {} // Initialize the state dispatch({ type: ActionTypes.INIT } as A); const store = { dispatch: dispatch as Dispatch<A>, subscribe, getState, replaceReducer, [$$observable]: observable, }; return store; } Copy the code
A Store instance consists of five parts,
getState
: Obtains the current statusdispatch
: callreducerGenerate a newstatesubscribe
: Adds a subscription functionensureCanMutateNextListeners
: Deep copycurrentListeners
Assigned tonextListeners
observable
: in order toObserver mode/responsiveExtension methods provided by the framework
Note that the type of action (actiontypes.init) is a concatenated random string:
const ActionTypes = { INIT: `@@redux/INITThe ${/* #__PURE__ */ randomString()}`.// There are two others, also with random numbers }; Copy the code
Type is a random string. If you go to the default branch of the switch statement, the return result will be the initial value of state. Therefore, not handling the default branch may cause some errors π¦π¦π¦.
1.2.2. getState
-
role
- Returns the current
state
- Returns the current
-
The core source
function getState() { if (isDispatching) { throw new Error( 'You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.',); }return currentState; } Copy the code
IsDispatching is the labels that are executing the Reducer. The Reducer is used to change state. Therefore, it is not safe to obtain the state at this time.
1.2.3. dispatch
-
role
- throughreducerchange
state
- Trigger all subscription updates
- throughreducerchange
-
The core source
function dispatch(action: A) { // Handle both edge cases and make sure the action parameter is available.try { isDispatching = true; currentState = currentReducer(currentState, action); } finally { isDispatching = false; } const listeners = (currentListeners = nextListeners); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } return action; } Copy the code
We take the action as an argument and return a new state via currentReducer, which is the reducer we write to.
Finally, the action is returned as is for later use by the middleware, which is explained in detail in the middleware section.
We see: return a new state each time. Think about the unnecessary rendering if you return a new reference every time the component is not optimized. πππ Remember, this is a big hole! We’ll sort it out at the end.
1.subscribe
-
role
- Adding a subscription function
-
The core source
function subscribe(listener: () => void) { // In the edge case, ensure that the listener is a function and the reducer is not executing.let isSubscribed = true; // Add to nextListeners, where the subscription functions are stored ensureCanMutateNextListeners(); nextListeners.push(listener); return function unsubscribe() { if(! isSubscribed) {return; } if (isDispatching) { throw new Error( 'You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api/store#subscribelistener for more details.' ); } isSubscribed = false; ensureCanMutateNextListeners(); // Delete the current subscription function const index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); currentListeners = null; }; } Copy the code
Adds a subscriber and returns a method to unsubscribe. What is the ensureCanMutateNextListeners here?
1.2.5. ensureCanMutateNextListeners
-
role
- Ensure that no changes are made to the currently available subscription functions
-
The core source
let currentListeners = []; let nextListeners = currentListeners; function ensureCanMutateNextListeners() { if(nextListeners === currentListeners) { nextListeners = currentListeners.slice(); }}function subscribe(fn) { ensureCanMutateNextListeners(); // A deep copy is made here nextListeners.push(fn); // Adding to the nextListeners array does not affect the currentListeners array } Copy the code
I was confused when I first saw this, but after many “debugs”, I finally found that this is to deal with some edge situation: if you delete or add a subscriber during the update, this update will not include the new subscriber, and will be carried in the next update.
1.3. Source code (2) : middleware
The reducer is a simple calculator that receives the state and action and returns the new state.
As you can imagine, we can’t change state directly in dispatch using asynchronous methods.
Next comes middleware, which enhances Dispatch π
constStore = createStore(Reducer, applyMiddleware)1, the middleware2, the middleware3,...). )Copy the code
1.3.1. applyMiddleware
-
role
- Receive multiple middleware as parameters
- The middleware is invoked in order
- And finally return a new dispatch (enhanced Dispatch)
-
The core source
export default function applyMiddleware(. middlewares) { return (createStore) = > (reducer, preloadedState) = > { const store = createStore(reducer, preloadedState); 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: (action, ... args) = >dispatch(action, ... args), };const chain = middlewares.map((middleware) = >middleware(middlewareAPI)); dispatch = compose(... chain)(store.dispatch);return { ...store, dispatch, }; }; } Copy the code
We’re using a functional programming concept called compose, which is the Onion ring model. We’ll look at the source code later, but we can simply think of it as getting an “enhanced Dispatch” that executes additional middleware logic each time it’s executed.
Let’s start with the steps for applyMiddleware:
- First of all, in accordance with theThe orderforThe middlewareInto the core
store
And the corestore
includinggetState
anddispatch
; - Then execute in orderThe middleware, which in turn returns the latest
createStore
Methods; - Then the dispatch is synthesized into an enhanced Dispatch through the chain-type composite middleware.
- And finally as a complete
store
To return.
So the middleware is a higher-order function: it receives the store and returns the createStore function that returns a new dispatch
Below is the source code for two commonly used middleware, you can see that both are very concise
-
redux-thunk
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) = > (next) = > (action) = > { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk; Copy the code
The three-tier function is returned because of a feature called Redux-Thunk, which allows us to dispatch a function for asynchronous operations.
-
redux-promise
export default function promiseMiddleware({ dispatch }) { return (next) = > (action) = > { if(! isFSA(action)) {return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) ? action.payload .then((result) = >dispatch({ ... action,payload: result })) .catch((error) = >{ dispatch({ ... action,payload: error, error: true }); return Promise.reject(error); }) : next(action); }; } Copy the code
This one is more conventional, returning a nested two-level function that handles asynchronous situations.
- First of all, in accordance with theThe orderforThe middlewareInto the core
1.3.2. compose
-
role
- Combine multiple functions in order to form a new function
-
The core source
export default function compose(. funcs:Function[]) { // The following two ifs deal with edge cases if (funcs.length === 0) { return <T>(arg: T) = > arg; } if (funcs.length === 1) { return funcs[0]; } // Use reduce to combine functions in order return funcs.reduce((a, b) = > (. args:any) = >a(b(... args))); }Copy the code
It is commonly used in functional programming to combine multiple functions. For example, compose(a, b, c), and you end up with a new function (… arbs) => a(g(c(… The args))). It can be simply understood as: The inner core of an onion is our dispatch. The so-called combination is to add layers of onion rings. Each onion ring is a middleware.
1.4. Source code (3) : Combinator
The combinator is the combineReducers
-
role
- Receive multiple
reducer
As a parameter - Finally return a new (enhanced) reducer
- The newreducerIt goes through each one in turn
reducer
Calculate, and finally return the new state
- Receive multiple
-
The core source
export default function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); const finalReducers = {}; // Retain the reducer that meets the specification for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i]; if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; }}// End up with all the reducer keys const finalReducerKeys = Object.keys(finalReducers); // Return a new Reducer return function combination(state = {}, action) { // Use the hasChanged variable to record whether the state was changed before and after let hasChanged = false; // Declare an object to store the next state const nextState = {}; / / traverse finalReducerKeys for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; / / reducer for execution const nextStateForKey = reducer(previousStateForKey, action); // Some code that handles edge cases. nextState[key] = nextStateForKey;// If two key comparisons are not equal, changehasChanged = hasChanged || nextStateForKey ! == previousStateForKey; }// The last keys array comparison is not equalhasChanged = hasChanged || finalReducerKeys.length ! = =Object.keys(state).length; return hasChanged ? nextState : state; }; } Copy the code
Take all the reducers and dispatch them from top to bottom, so action. Type must not be repeated!
becauseThis functionIs the function ofCombination reducersSo in the endreturnA newreducer
1.5. Process combing
- ReduxWhen initializing, it first determines whether middleware is used. If no middleware is used, it initializes by default. If middleware is used, it passes
applyMiddleware
Function to create applyMiddleware
The receivedparameterIs the defaultcreateStore
Method, which will eventually return a newcreateStore
Method, in whichdispatch
The function will beThe middlewareTo enhance- In 2.1.
applyMiddleware
Inside,The middlewareExecute the command in sequence to obtainMiddleware functionArray form of - 2.2. Then pass
compose
All of theMiddleware functionMerge into onedispatch
- 2.3. The final will beTo strengthenthe
dispatch
And the rest of thestore
Back together
- In 2.1.
combineReducers
To accept an object type as an argument, it will be multiplereducerCombine into a function and executedispatchFrom the top down through each of themreducerGet the finalstate
2. React-Redux
The react-Redux principle is simple, but the source code is slightly complicated to handle many edge cases. Extracting the core can be summarized as follows
- Create a Context and use the Provider to share the Store with the child components.
- Class componentsthroughHigher-order functions
connect
Complete the subscription update; throughmapStateToProps
Implement props injection and passmapDispatchToProps
Implementation of dispatch injection; - Function componentthrough
useSelector
Complete subscription update and implement state injection; throughuseDispatch
Return to the latestdispatch
.
2.1. The connect
-
role
- connectIs a high-level component, done inside the componentTo subscribe to, implementation,
mapStateToProps
εmapDispatchToProps
- connectIs a high-level component, done inside the componentTo subscribe to, implementation,
-
Core principle code
export const connect = (mapStateToProps, mapDispatchToProps) = > ( WrapperComponent, ) = > (props) = > { const store = useStore(); const { getState, dispatch } = store; const forceUpdate = useForceUpdate(); // Client rendering using useLayoutEffect useLayoutEffect(() = > { const unSubscribe = store.subscribe(() = > { forceUpdate(); }); return () = >unSubscribe(); } []);const stateToProps = mapStateToProps(getState()); let actionToProps = { dispatch }; if (typeof mapDispatchToProps === 'object') { actionToProps = bindActionCreators(mapDispatchToProps, dispatch); } else if (typeof mapDispatchToProps === 'function') { actionToProps = mapDispatchToProps(dispatch); } return <WrapperComponent {. props} {. stateToProps} {. actionToProps} / >; }; const useForceUpdate = () = > { const [, forceRender] = useReducer((s) = > s + 1.0); return forceRender; }; Copy the code
Implement mapStateToProps source and mapDispatchToProps or more complex, through the match and the factory function mapStateToPropsFactories, mapDispatchToPropsFactories completed, But the core code is what the above code looks like, with bindActionCreators as follows
function bindActionCreators(actionMap, dispatch) { const actions = {}; for (const action in actionMap) { actions[action] = bindActionCreator(actionMap[action], dispatch); } return actions; } function bindActionCreator(action, dispatch) { return (. args) = >dispatch(action(... args)); }Copy the code
It’s worth noting in two ways,
-
1. Implement forceUpdate function component, which is also recommended on the website.
const [, forceRender] = useReducer((s) = > s + 1.0); Copy the code
-
2. Why use useLayoutEffect? The source code makes a judgment here, server rendering use useEffect; Use useLayoutEffect for client rendering.
In React, useLayoutEffect componentDidMount and componentDidUpdate are executed synchronously. UseEffect belongs to asynchronous execution, that is, after the end of this update phase, it is executed in the next task schedule.
This means that if a client uses useEffect to subscribe during rendering, the subscription will be executed after the update task is complete, and any actions that trigger the update in the current update task will be lost.
There are two reasons why the server uses useEffect for rendering. One is that the server uses useLayoutEffect to render and it will give a warning. Second, dom already exists when the server renders, and js may still be loaded at this time, so there is no delay in scheduling.
-
2.2. UseSelector and useDispatch
-
role
useSelector
in-houseTo subscribe toTo return the desiredstate
useDispatch
Return to the latestdispatch
-
Core principle code
export const useSelector = (selector) = > { const store = useStore(); const forceUpdate = useForceUpdate(); useLayoutEffect(() = > { const unSubscribe = store.subscribe(() = > { forceUpdate(); }); return () = >unSubscribe(); } []);const ret = selector(store.getState()); return ret; }; export const useDispatch = () = > { const store = useStore(); return store.dispatch; }; const useStore = () = > { const store = useContext(Context); return store; }; Copy the code
3. Think and summarize
-
Redux and Vuex
- Vuex relies on Vue, although it is implemented in a clever way, but it cannot be used without Vue; Redux is a Javascript library that can be used anywhere;
- Vuex is extended through plug-ins, and multiple plug-ins can be extended with the onion model; Redux extends through middleware;
- VuexBasically no difficulty to get started;ReduxNeed to knowFunctional programmingFor example
compose
,The onion model,Pure functionsAnd so on; - VuexOf the update granularityVue, belongs to directional update; whileReduxUpdate granularity benchmarking based on publish-subscribeReact, but need to use
connect
After processing can be implementedAutomatic updates.
-
Some notes
reducer
It’s a pure function, so don’t put any bands in itSide effectsSuch as publish subscribe;mapStateToProps
andmapDispatchToProps
Both have a second argument[ownProps]
If this parameter is specified, the component will listen for changes to the Redux store. Otherwise, it will not listen. OwnProps is the current component’s props. MapStateToProps and mapDispatchToProps will be recalculated. Use this with caution!
-
Meaning of the Default branch in the Reducer function
When Redux is initialized, it calls the Init level Dispatch once to initialize the store. In this case, it uses the default branch of the switch statement. If it is not written, the default value of store will be undefined.
-
The pros and cons of a single data source
Perhaps a single data source is really easy to manage and easy to go back to. But there are a number of pitfalls with returning a new state each time. Imagine: We send a dispatch to get a new state. React doesn’t care if the actual contents of the current state have changed. It only sees the difference between the two references and updates. Redux is a lot like React in this respect, but a single reference data warehouse like Vuex or Mobx performs better.
-
UseEffect and useLayoutEffect
-
UseEffect: During React rendering, changing the DOM, adding subscriptions, setting timers, logging, and performing other actions that contain side effects in the body of a function component are not allowed, as they can create unexplained bugs and break UI consistency. Use useEffect for side effects. Functions assigned to useEffect are delayed until the component is rendered to the screen. Think of Effect as an escape route from React’s purely functional world to the imperative world.
-
UseLayoutEffect: Its function signature is the same as useEffect, but it calls Effect synchronously after all DOM changes. You can use it to read the DOM layout and trigger rerendering synchronously. The code inside the useLayoutEffect is executed synchronously before the browser performs the drawing. Use standard useEffect whenever possible to avoid blocking visual updates.
-
Redux, React-Redux source code