React-redux has always been used in projects to use redux, but THE principle of redux has never been thoroughly understood!! So recently class is little ~ can study carefully!! 🤔 🤔
As I read, I ask myself a lot of questions, and then I go around looking for answers, so I get something out of school!
This article is for those of you who have used Redux
As for the source version, 4.0 is still used, and TS version is not mentioned. I think if the TS version is used for analysis, it will involve explaining a lot of type judgment, which is not conducive to the readability of the article
The article is a bit long, and the general contents are as follows: Util folder –actionTypes –isPlainObject Why redux does this? / / warnings SRC folder / / index / / createStore / / Currify / / higher order functions / / help understand applyMiddleware — Compose (function combination, reduce, reduceRight method) –appMiddlewares “-bindActionCreators” – “Combiner Creators call Replacer Creators” The reducer must be a pure reducer function. The reducer must be a reducer function
Utils folder
We usually start with the index file when we read the source code, but I thought it would be useful to look at the utility class functions first, so we’ll start with the utils file:
actionTypes
// Generate a random string const randomString = () => math.random ().toString(36).substring(7).split('').join('.') const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` } export default ActionTypesCopy the code
This file is used to define three reserved types of redux:
INIT(Reducer initialization type)
REPLACE(Reducer replacement type)
PROBE_UNKNOWN_ACTION(Random type)
At the same time, to ensure the uniqueness of the three types, a randomly generated string is added to the end of each type
*isPlainObject
Export default function isPlainObject(obj) {// Obj must be a non-empty object if (typeof obj! = = 'object' | | obj = = = null) return false let proto = obj along the prototype chain / / lookup, up until the proto (father) is null when the stop at the next higher level, While (object.getProtoTypeof (proto)! == null) {proto = object.getProtoTypeof (proto)} Null => an object => plainObject return Object.getProtoTypeof (obj) === proto}Copy the code
This function may seem mundane on the surface, mainly to determine if an object is a simple object, but it has some interesting features
A plain object is an object without an official definition.
In the above function, the argument obj can only be an Object created from a literal ({}), or from a new Object(),
Obj cannot be an Object created with Object.create(), even if object.create (null),
Of course, when looking at the source code, why this judgment can not be directly written, clearly can achieve the same function 🧐🧐
let proto = Object.getPrototypeOf(obj) return !! proto && Object.getPrototypeOf(proto) === nullCopy the code
Later I saw in a Commit history on Redux’s Github,He used to write it like this:
There is always some justification for change, as discussed in redux’s Github issue,This is what the author says:The translation reads as follows:
It handles cross-domain objects by iterating through the prototype chain until it reaches the Object, and checking if the “Object” has the same prototype as the normal Object we need to determine.
For objects from iframe, this object is different from our native objects because they come from two different JavaScript contexts. Therefore, we use this technique to get the Object definition of an Object instance.
It’s a good performance boost because you can short-circuit them in a fair number of cases before going into the slower toString() calls (which are the parts that slow lodash/isPlainObject and IS-Plain-Object).
In the third sentence, I understand that it can improve performance by terminating some work before calling toString() (if you have a better understanding, let me know in the comments section 😝😝😝😝).
In general, the benefits of using circular judgment are as follows:
1. Resolve cross-realm object issues
2. Can have higher performance
warning
export default function warning(message) { if (typeof console ! == 'undefined' && typeof console.error === 'function') { console.error(message) } try { throw new Error(message) } catch (e) {} }Copy the code
This function encapsulates the error message
index
function isCrushed() {} if ( process.env.NODE_ENV ! == 'production' && typeof isCrushed.name === 'string' && isCrushed.name ! == 'isCrushed' ) { ... Warning Message} export {createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes}Copy the code
In addition to exposing methods, the index file serves as an entry point to the code:
I don’t know if you noticed the following function:
function isCrushed() {}
This is a pseudo function
In the case of compressed functions, the function name is replaced with a single letter or even an _
Therefore, we can use this pseudo-function to determine whether code compression exists in the development environment
If such a case exists, an error is thrown
if ( process.env.NODE_ENV ! == 'production' && typeof isCrushed.name === 'string' && isCrushed.name ! == 'isCrushed' ) { warning( 'You are currently using minified code outside of NODE_ENV === "production". ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' + 'to ensure you have the correct code for your production build.' ) }Copy the code
Before we look at the source code, let’s think about how a store would be created in a real Redux project:
In this case, enhancer is the equivalent of a collection of middleware components composed by compose via the applyMiddleware application (which may sound like a mouthful 😸).
createStore
CreateStore is redux’s core block, and its job is to create a Store object, And exposed ensureCanMutateNextListeners, getState, subscribe, dispatch, replaceReducer, observables that several methods
export default function createStore(reducer, preloadedState, Enhancer) {/ / some judgment function ensureCanMutateNextListeners () {} function getState () {} function the subscribe () {} function dispatch() {} function replaceReducer() {} function observable() {} dispatch({ type: ActionTypes.INIT }) }Copy the code
[$$observable]
The first line of the createStore code is:
import $$observable from 'symbol-observable'
What is his role?
In thisThe readme of NPM package”Is described as followsIt means to matchRedux
,RxJS
In such libraries, our objects become Observables, and we need to avoid calling error, complete, and unsubscribe after calling error, complete, and unsubscribe
In actual development, we usually don’t use this method 👻👻👻👻
export default function createStore(reducer, preloadedState, enhancer) {
}
Copy the code
CreateStore receives three parameters — reducer (function), preloadedState (initial state), enhancer (function, a higher-order function). These three parameters are clearly explained in the official document:
Some of the judgment
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}
Copy the code
This determination is intended to guarantee the single enhancer principle. If there are multiple enhancers, they need to be grouped together by compose
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
Copy the code
The preloadedState parameter is optional, so when the second parameter of createStore is a function and the third parameter is undefined, the second parameter is passed in enhancer by default. PreloadedState and enhancer cannot be functions at the same time. Therefore, preloadedState cannot be functions at the same time. PreloadedState is of type any. That was a bit of a misnomer, but I found the answer in Github issues:
It’s not really that important, but it would be nice to have an official explanation 👻🤡😸🤠 college 👻 👻
if (typeof enhancer ! == 'undefined') { if (typeof enhancer ! == 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) }Copy the code
Enhance the function of the store, let it have the function of the third party, such as middleware. ApplyMiddleware () is an enhancer (talking about applyMiddleware specifically 🧐)
This is also equivalent to leaving the store creation process entirely to the middleware in the presence of enhancer
Build store initialization
CurrentState let currentState = preloadedState // store the current event listener let CurrentListeners = [] // The current event listeners are referenced. Let nextListeners = currentListeners // Whether Actions (false) are currently being sent falseCopy the code
ensureCanMutateNextListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
Copy the code
This is the internal method of createStore, and in code, it just feels normal to copy currentListeners and assign values to nextListeners when nextListeners are equal to currentListeners. (More on why 👻 later)
getState
function getState() { //... If an action is being issued, the call will throw an error return currentState}Copy the code
Return the current state directly
subscribe
Function subscribe(listener) {// If (typeof listener! == 'function') { ... } // An error is thrown when the actions are being dispatched. } / / subscription state is set to true the let isSubscribed = true / / copy of the current currentListeners ensureCanMutateNextListeners () // Listeners are added to the listeners. Push (listener) return function unsubscribe() { }}Copy the code
unsubscribe
function subscribe(listener) { ... Return function unsubscribe() {// Unsubscribe if (! IsDispatching) {return} // An error is raised while the actions are being dispatched. } // Unsubscribe isSubscribed = false // Copy again, Cancel the current listeners ensureCanMutateNextListeners () const index = nextListeners. IndexOf (the listener) nextListeners. Splice (index, 1) currentListeners = null } }Copy the code
Here, let’s take a look backensureCanMutateNextListeners
, why copy and not use directlycurrentListers
The idea is to avoid calling subscribe/unsubscribe during dispatch
dispatch
Function dispatch(action) {// Action must be pure if (! isPlainObject(action)) { ... } // If (typeof action.type === 'undefined') {... } // If actions are being sent, an error is reported. Avoid calling dispatch from the reducer, and dispatch calls the reducer again, forming a loop... if (isDispatching) { ... } try {// Set the current state to isDispatching = true // Pass in the reducer the current state and the state to change, CurrentState = currentReducer(currentState, action)} finally { // Assign values to currentListeners and Listeners respectively, and trigger listeners one by one; const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } return action }Copy the code
replaceReducer
Function replaceReducer(nextReducer) {// nextReducer must be a function if (typeof nextReducer! == 'function') { ... } // change currentReducer currentReducer = nextReducer // notify all reducer state changes dispatch({type: actiontypes.replace})}Copy the code
For the use scenario of replaceReducer, I only saw code division in the official website, and hot update mechanism and dynamic load different reducer in other places
observable
RxJS-based middleware for Redux that allows developers to work with async actions
Copy the code
He is a middleware based on RxJS, used for asynchronous action, in our daily use, actually not how involved, just ignore it 🤔
Complete the initialization of the Store
dispatch({ type: ActionTypes.INIT })
Copy the code
Expose some methods
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
Copy the code
Currization, higher order functions
Before we get into compose and Applymiddleware, we need to do a quick primer on what compose and applymiddleware are about, so if you already know something, skip it!!
Conceptually:
- Higher-order functions: are receiving functions as
parameter
, or a function as a returnThe output
thefunction
React-redux connect()();Copy the code
- Corrification: acceptance
Multiple parameters
Is converted to accept aSingle parameter
(the first argument to the original function), and returnsAccept the remaining parameters
And return the resultThe new function
(I think of currization as an application of higher-order functions)
Function add(x, y){return x + y; Function (x) {return function(y) {return x + y}} curryAdd(1)(2) // 3Copy the code
None of the above are covered in detail, they cover a lot of functional programming patterns, and if they were covered in detail, it would be a long story, but in terms of learning redux, let’s just get a rough idea of what they mean
compose
Compose refers to a combination of functions, which is an important idea in functional programming, but not limited to it
The function of REDUx is to realize the combination of any, various and different function modules, so as to enhance the function of REDUx
export default function compose(... Funcs) {if (funcs.length === 0) {return arg => arg} If (funcs.length === 1) {return funcs[0]} 😝 return funcs. Reduce ((a, b) => (... args) => a(b(... args))) }Copy the code
Barring some judgment, the core code for Compose is that last sentence
- First of all, yes
Array.prototype.reduce
Method, reduce method is actually very powerful, more on thatThe document
His syntax arr. Reduce (callback,[initialValue]) his function from left to right, Callback: The function contains four arguments - previousValue (the value returned from the last callback call, Or the provided initialValue (initialValue) -currentvalue (the element in the array that is currently being processed) -index (the index of the current element in the array) -array (the array that was called) initialValue (as the first call Const arr = [1, 2, 3] const sum = arr. Reduce ((sum, val) => {return sum + val; }, 0); //sum: 6Copy the code
The law of the composer
compose(a, b) => (... args) => a(b(... args)) compose(a, b, c) => (... args) => a(b(c(... args)))Copy the code
That is, it does it in the reverse order of the input order, the result of the previous function as the input of the next function
This is very similar to koA’s onion ring model(aop)
Here’s another picture to illustrate:In fact, we can also find that this is not reduceRight method! = >The documentIn other words, the compose function can be written like this:
export default function compose(... funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } const last = funcs[funcs.length - 1]; const rest = funcs.slice(0, -1); return (... args) => rest.reduceRight((composed, f) => f(composed), last(... args)) }Copy the code
And according to the submitted records, the reduceRight method was really used before
As for the reason for the change, the author replied as follows:
That means it’s one to three times faster in terms of performance
Finally, note that compose returns a function, which is composed (a,b)() only when it is called.
applyMiddleware
By default, the Redux store created by createStore() does not use Middleware, so it only supports synchronous data streams.
We can use applyMiddleware() to enhance createStore(). This is not necessary, but it will help you to describe asynchronous actions in an easy way.
Not using Middleware:After using Middleware:
Look at the source code again:
export default function applyMiddleware(... middlewares) { return createStore => (... args) => { const store = createStore(... args) 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: (... args) => dispatch(... args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(... chain)(store.dispatch) return { ... store, dispatch } } }Copy the code
We go through it paragraph by paragraph
return createStore => (... args) => { const store = createStore(... args) ... }Copy the code
Before analyzing, a recap:
Use of Applymiddleware from Redux
const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
Copy the code
The call to enhancer(Applymiddleware) in createStore when enhancer exists and is a function
return enhancer(createStore)(reducer, preloadedState)
Copy the code
In combination, this call is equivalent to
applyMiddleware(logger)(createStore)(todos, ['Use Redux'])
or
applyMiddleware(middlewware)(createStore)(reducer, preloadedState)
Copy the code
What does this format make us think of, Currization, right?! And think about the order in which corrification runs!
With that in mind, let’s take a look at the source code
// createStore is the createStore function... Reducer and preloadedState return createStore => (reducer and preloadedState) Const store = createStore(... args) => {// Create a store using the method passed in. args) }Copy the code
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
Copy the code
The meaning of this code is that the call to Dispatch is prohibited during middleware builds. Why?
The reason for the existence of middleware is to enhance the functions of Redux, that is, to enhance the functions of Dispatch. Before the dispatch is assembled, it does not cover the original Dispatch of Redux, which may lead to some functions that cannot be realized.
Redux, for example, originally only supports synchronous actions, but can only support asynchronous actions if middleware is used. If this middleware is not built, calling Dispatch will not be able to distribute asynchronous actions
At the same time, the purpose of defining dispatches with lets is to make it easy to change references to dispatches
const middlewareAPI = { getState: store.getState, dispatch: (... args) => dispatch(... Middlewares.map (Middleware => middlewareAPI) const chain = middlewares.map(middleware => middlewareAPI) Dispatch = compose(... chain)(store.dispatch)Copy the code
The middlewareAPI is a built-in object for applyMiddleware
The middlewareAPI is passed to each middleware as a parameter when traversing the middleware (middlewares.map), passing the result back to chain
Combine redux-Thunk source code to analyze this is a process
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 above code is currified. Let’s convert it to ES5:
Function createThunkMiddleware(extraArgument) {return function({dispatch, Return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) {return function(next) { Dispatch return function(action) { If (typeof Action === 'function') {return action(dispatch, getState, extraArgument); } // If the argument is not a function, call the original dispatch return next(action); }; }}}Copy the code
It’s a little bit convoluted here, but let’s rearrange the process a little bit
-
Middleware is in the form of ({getStore, dispatch}) = > (next) = > (action) = > {… }
-
A middlewareAPI object is passed to the first-layer function of the middleware through traversal
-
(next)=>(action)={… } function
-
Compose =>(next)=>(action)={… }
-
Compose is composed in such a way that the next (action)={… } as an argument to the next function, next
-
The next of the last combinatorial function is store.dispatch
-
Finally, the compose function (action)={… } assigns to Dispatch
return { ... store, dispatch }Copy the code
Returns the created store and wrapped dispatch
Here’s another example of what middleware can do: 👻👻👻👻 port
------>
bindActionCreator
Action Creator is a function that creates actions. It is like a factory. It is only responsible for action creation (create), but not dispatch (dispatch).
So what does bindActionCreator do? He basically upgraded the factory so that it could both create and distribute actions
Let’s take a look at the definition on the official website:
Convert an object with a value of a different Action Creator into an object with a key of the same name. Each Action Creator is also wrapped with Dispatch so that they can be invoked directly.
Normally you can call Dispatch directly on a Store instance. If you use Redux in React, react-Redux will provide a dispatch function that you can call directly.
Look at the source code of bindActionCreator
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
Copy the code
He returns a function that implements the functionality of the Dispatch action
Let’s go back to Binge Creators
Export default function bindActionCreators(actionCreators, Dispatch) {// When actionCreators is a single function, Call bindActionCreator directly if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, Dispatch)} // An error is reported when actionCreators is not an object. If (Typeof actionCreators! == 'object' || actionCreators === null) { ... Throw an error} // Create a new object const boundActionCreators = {} // Iterate over the actionCreators object so that each of its entries, Call bindActionCreator for (const Key in actionCreators) {const actionCreator = actionCreators[key] if (typeof) actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, Dispatch)}} // Finally returns a new object return boundActionCreators}Copy the code
So for this actionCreators
When other files are introduced, it becomes an object:
For a single actionCreator, after calling bindActionCreator, it becomes
text => dispatch(addTodo('text');
Copy the code
For multiple ActionCreators, after calling bindActionCreators, this will become
{
addTodo : text => dispatch(addTodo('text'));
removeTodo : id => dispatch(removeTodo('id'));
}
Copy the code
Thus the dispatch call is implemented
combineReducers
In projects, we often use combineReducers like this to combine multiple different Reducer functions
Let’s look at the source code
Export default Function combineReducers(reducers) {Reducer yes, Key const reducerKeys = object. keys(reducers) // ReducerReducers = {} for (let I = 0; i < reducerKeys.length; I ++) {const key = reducerKeys[I] if (process.env.node_env! == 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } // When Reducer is function, Equivalent to a reducer copy to finalReducers if (Typeof Reducers[key] === 'function') {finalReducers[key] = reducers[key]}} const FinalReducerKeys = Object.keys(finalReducers) // In non-production environment, Let unexpectedKeyCache if (process.env.node_env! == 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { // Check that the reducer in finalReducer received a {actiontypes.init} or actiontypes.probe_unknown_action (), AssertReducerShape (finalReducers)} Catch (e) {shapeAssertionError = e} // This returned function is a combined reduce // Return function combination(state = {}, Action) {// If (shapeAssertionError) {throw shapeAssertionError} if (process.env.NODE_ENV ! == 'production') { For example, check whether // state is a pure object // reducers is empty // inputState key does not filter inputState in Reducers and unexpectedKeyCache //... const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, UnexpectedKeyCache) if (warningMessage) {warning(warningMessage)}} // define a change identifier let hasChanged = false // define new state Const nextState = {} // Go through the reducerKeys for (let I = 0; i < finalReducerKeys.length; I ++) {// Obtain key and Reducer const key = finalReducerKeys[I] const Reducer = finalReducers[key] // Obtain state const reducer corresponding to the current key PreviousStateForKey = state[key] // Change state const nextStateForKey = Reducer (previousStateForKey, If (typeof nextStateForKey === 'undefined') {// Reducer cannot return undefined const errorMessage = getUndefinedStateErrorMessage(key, NextState [key] = nextStateForKey nextState[key] = nextStateForKey So every time the change of the reducer is always to return to a new state object hasChanged = hasChanged | | nextStateForKey! == previousStateForKey} // When using CombineReducers to call replaceReducers and remove one of the reducers passed to it, Not update status hasChanged = hasChanged | | finalReducerKeys. Length! == object.keys (state).length // Return state hasChanged? nextState : state } }Copy the code
Reason for repeating hasChanged: discussion in Github Issue
Just a little bit about what I’ve learned
- function isCrushed() {}
The name attribute of the pseudo function is used to determine whether code compression exists in the development environment
- Compose the composition function
Reduce method and reduceRight method
- The discriminant method of pure function (isPlainObject)
Lodash, jq, has in the story
- Higher order functions, Currization
In redux source code, a lot of places are used to higher-order functions, especially middleware that piece, at first will only feel like looking at the clouds in the fog, read more feel like this writing is very concise
- Publish and subscribe
Publish-subscribe functions and observer patterns (they are often lumped together, but there are subtle differences, although they are functionally similar)
- Redux’s three principles
Single data source: The state of the entire application is stored in an object tree that exists in a single store
State is read-only: the only way to change State is to trigger an action, which is a generic object that describes events that have occurred.
Use pure functions to perform modifications: To describe how an action changes the State Tree, you need to write reducers
- Pure functions
The same input always produces the same output
The only way a function interacts with the outside world is by passing in parameters and returning values
No side effects
- Why does reduer have to be pure
Let’s look at this code again, and we’re doing it by judgmenthasChanged
Whether it istrue
To determine whether to return the new state or the old statestate
the
HasChanged is again determined by comparing the references to nextStateForKey and previousStateForKey
So look at this code again:nextStateForKey
Is through thepreviousStateForKey
To obtain the
ifreducer
Instead of doing it purely as a function, that’s how we’re going to write itreducer
And that would lead tostate
Unable to update becausestate
The reference is unchanged
Why do we make judgments based on references instead of attribute values? Deep comparisons are performance-consuming such as deep copy, so Redux simply imposed a pure function constraint on reducer
- Redux data flow
😝😝😝😝😝😝😝
The user triggered an action through an interaction event, and the action was packaged by middleware and sent to the reducer
Reducer changes state according to the action instructions. The page View has been monitoring the data changes in the store, and the new state will be rendered on the page after the changes, so as to achieve user response
- Why can’t Redux just send out asynchronous actions? Check it out for yourself.
To summarize: It is possible to implement asynchronous data flows without middleware, but it is not convenient to do so in large projects
Because we might be executing the same action in different components, you might want to debounce some actions. Or some local state (such as auto-increment ID) is more closely related to Action Creator
Therefore, from a maintenance perspective, it would be better to extract Action Creator
Redux Thunk or Redux Promise is just a grammar sugar, which helps us dispatching Thunk or Promise, but we are not necessarily using him
This means that with middleware, we don’t have to think about asynchronous synchronization and can focus on components. Redux-saga was also mentioned in the discussion, which is interesting. Next time, 😝
Think of… The list goes on, like Redux, mobx differences, functional programming blah blah blah
conclusion
The process of learning the source code is still very difficult 😵, need to take a moment to guess why, for appyMiddleware this part, go to the debugger!
There may be some places in the article that need not be rigorous, welcome to discuss with me in the comment area! 😸
The reference link 🔗 is basically in the original article and the redux website link is at the end