preface
Before learning redux, redux-Thunk source code, next work hard, to understand the react-Redux source code.
P.S. The source code version number studied here is 7.2.4
role
Redux is a separate library that can be used in multiple libraries or frameworks. When we use redux in conjunction with a UI library (framework), we often use a UI binding library as a bridge between them.
Redux itself is a standalone library that can be used with any UI layer or framework, including React, Angular, Vue, Ember, and vanilla JS. Although Redux and React are commonly used together, they are independent of each other.
If you are using Redux with any kind of UI framework, you will normally use a “UI binding” library to tie Redux together with your UI framework, rather than directly interacting with the store from your UI code.
Its main functions are:
react-redux
orredux
The official team maintains whenredux
When updated, the library is updated;- Improved performance, as is usually the case, when a
component
The whole tree is rerendered when it changes, butreact-redux
Internally help us do optimization, improve performance; - The community is strong.
Source code analysis
The source SRC /index.js file is exposed as:
export {
Provider,
connectAdvanced,
ReactReduxContext,
connect,
batch,
useDispatch,
createDispatchHook,
useSelector,
createSelectorHook,
useStore,
createStoreHook,
shallowEqual,
}
Copy the code
ReactReduxContext
CreateContext creates a global context using react.createcontext. So react-Redux is essentially using context, you need to use context to pass stores to nested components, and when the state in the store is updated, you also need to update the state of the nested child components, So the store and description are saved in the context later, simplifying the use of redux.
// src/components/Context.js
import React from 'react'
export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)
if(process.env.NODE_ENV ! = ='production') {
ReactReduxContext.displayName = 'ReactRedux'
}
export default ReactReduxContext
Copy the code
batch
This is a variable related to batch updates in React. Batch update, according to my understanding at this stage: If the update is called N times in a row, React will not trigger view rerendering every time, but will update the view when the final result is obtained directly after the update.
// src/utils/reactBatchedUpdates
export { unstable_batchedUpdates } from 'react-dom'
// src/index.js
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
Copy the code
Provider
The purpose of the Provider is that you can inject the state of a store into a parent component, and then any component wrapped by the parent component can use the state of the Store without having to manually reference the Store in every file.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider >,
document.getElementById('root')
);
Copy the code
A Provider is a component, so it has the same basic structure as a normal component. According to the documentation, it takes three parameters: Store, Chidren (which is usually our App component), Context, and (if so, provide the same context for all connected components).
As we know above, React-Redux mainly uses context. Here, it is not difficult to analyze the basic architecture of Provider:
import { ReactReduxContext } from './Context'
function Provider({store, context, children}){
const Context = context || ReactReduxContext;
return (
<Context.Provider value={store}>{children}</Context.Provider>)}Copy the code
We need to do something with the Context and store in the Provider. Before we do that, let’s analyze a few small methods:
createListenerCollection
Each node has a callback function and two Pointers: a pointer to the previous node (prev) and a pointer to the next node (next).
CreateListenerCollection specifies a first to the head of the list and a last to the tail of the list. It also defines the following methods:
-
Clear: clear the first and last Pointers, that is, clear the list;
-
Notify: Iterates over each node in the listener and calls their callback. The batch mentioned above is also used here. Guess is traversal node call function, batch can ensure that the function is called after the completion of a one-time update, to achieve the role of batch processing, rather than every callback function, update once;
-
Get: Iterates through the entire list, saving each node in sequence in the Listeners array, and returns it;
-
Subscribed = true to indicate that the new nodes had been listened to, and then added them as if they had been added to the list:
- Create a new node with
Callback, Prev, and next
Property, whereprev
The assignment forlast
(because the new node is placed after the last node); - If the current list is empty, then this node is the head node, otherwise, the previous node of the current node
next
It points to this node.
Finally, it returns a function to cancel listening on the node:
- if
isSubscribed
forfalse
orfirst
If (list) is empty, return directly; - Set up the
isSubscribed
forfalse
; - Delete logic can be a bit convoluted, let’s use an example where the current node is B:
- if
B
There’s the next nodeC
So we’re going toC
theprev
Point to theB
The last node of; - Otherwise, that is,
B
There is no next node, so at this pointlast
Points to theB
, then it should belast
Point to theB
The last node of; - if
B
We have the last nodeA
, thenA
thenext
Should be directedB
The next node of; - Otherwise, say
B
That’s at the head of the listfirst
It points toB
, should be modified toB
The next node of.
- if
- Create a new node with
import { getBatch } from './batch'
function createListenerCollection() {
// The React update mechanism is related to batch. When multiple values are updated together, instead of rerendering each time a value is updated, the react update mechanism is related to batch
const batch = getBatch()
let first = null / / the head pointer
let last = null // The tail pointer indicates that the list is bidirectional
return {
// Empty the pointer
clear() {
first = null
last = null
},
notify() {
batch(() = > { // The callback function is called for batch processing
let listener = first // Iterate through the list from the beginning, executing each callback function
while (listener) {
listener.callback()
listener = listener.next
}
})
},
get() {
let listeners = []
let listener = first
while (listener) {
listeners.push(listener)
listener = listener.next
}
return listeners // Return a callback function for all subscriptions
},
subscribe(callback) {
let isSubscribed = true // Indicates a subscription
let listener = (last = { // The listener is a node with a bidirectional pointer
callback,
next: null.// The tail of the new node is null, so it is null
prev: last, // The node before the new node must be the previous node
})
if (listener.prev) {
// If the new node has a previous node, then its previous node points to it with the next pointer
listener.prev.next = listener
} else {
// If not, the new node is considered as the first node
first = listener
}
return function unsubscribe() {
// If there is no subscription, or the list is empty, then there is no need to unlisten
if(! isSubscribed || first ===null) return
// Unsubscribe
isSubscribed = false
// Since the node is deleted, the pointer to the node before and after it needs to be changed
// You may also need to modify the first and last Pointers
if (listener.next) {
listener.next.prev = listener.prev
} else {
last = listener.prev
}
if (listener.prev) {
listener.prev.next = listener.next
} else {
first = listener.next
}
}
},
}
}
Copy the code
Subscription
The Provider passes the state in the Store to each component and changes the store in each component when the state is updated. Therefore, you need to listen on the store in real time and support the operation of unbinding the listener. So it encapsulates a function called Subscription, which means to subscribe. This function will be used later in the implementation of other methods, so you need to clear your mind about it.
encapsulates the subscription logic for connecting a component to the redux store, as well as nesting subscriptions of descendant components, so that we can ensure the ancestor components re-render before descendants
Encapsulate the subscription logic used to wire components to the Redux store, as well as nested subscriptions to the descendant components so that we can ensure that the ancestor components re-render before the descendant components
// src/utils/Subscription.js
const nullListeners = { notify(){}}export default class Subscription {
constructor(store, parentSub) {
this.store = store // Store in redux
this.parentSub = parentSub // context.store
this.unsubscribe = null // Subscribe to the object
this.listeners = nullListeners // Subscribe to the callback function
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)}// Add listeners to nested components
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
// Notify the nested component that state has changed
notifyNestedSubs() {
this.listeners.notify()
}
// The callback function to execute when the store changes
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
// Check whether a subscription is available
isSubscribed() {
return Boolean(this.unsubscribe)
}
// Add a subscription
ParentSub has a higher priority than store
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
// Initialize Listeners to create the linked list
this.listeners = createListenerCollection()
}
}
// Unsubscribe
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
Copy the code
Subscription creates a listeners object that listens on the store and notifies each component, or parentSub, adds a listener, and iterates over it
useIsomorphicLayoutEffect
Before the study useIsomorphicLayoutEffect, stroke our useEffect and useLayoutEffect difference:
useEffect
: whenrender
When it’s over,callback
The function is executed, and because it is asynchronous, it does not block browser rendering;useLayoutEffect
: If you wereuseEffect
Need to deal withDOM
Operation, can be useduseLayoutEffect
, the screen will not flicker. It will beDOM
Immediately after completion, but before the browser does any drawing, so it blocks the browser’s rendering.- No matter
useLayoutEffect
oruseEffect
Can’t inJavascript
Execute before the code has finished loading. Introduced in the server-side rendering componentuseLayoutEffect
Code will triggerReact
Warning. To solve this problem, you need to move the code logic touseEffect
In the.
The Provider of
import { useEffect, useLayoutEffect } from 'react'
export const useIsomorphicLayoutEffect =
typeof window! = ='undefined' &&
typeof window.document ! = ='undefined' &&
typeof window.document.createElement ! = ='undefined'
? useLayoutEffect
: useEffect
Copy the code
To solve the above problems, the browser rendering uses useLayoutEffect and the server rendering uses useEffect. UseLayoutEffect is used to ensure that the store’s subscription callback is executed on the last Render commit, otherwise we might lose the update when the store’s update occurs between Render and Effect.
We also need to ensure that the subscription is created asynchronously, otherwise the states we observe might not be consistent with those in the Store when an update to a store occurs before the subscription is created.
The source code
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '.. /utils/Subscription'
import { useIsomorphicLayoutEffect } from '.. /utils/useIsomorphicLayoutEffect'
function Provider({ store, context, children }) {
// useMemo recalls the internal function if the dependency changes
// When the store changes, the contextValue is recalculated
const contextValue = useMemo(() = > {
// Create Subscription object
const subscription = new Subscription(store)
// Bind notifyNestedSubs to onStateChange
subscription.onStateChange = subscription.notifyNestedSubs
// In React-redux, contextValue contains both of these attributes
// Subscription is a listener and can be passed as the store changes
// The component that subscribed to the store can also be unlistened
return {
store,
subscription,
}
}, [store])
// Recalculate the current value of getState when the store changes
const previousState = useMemo(() = > store.getState(), [store])
// Triggers the following logic when previousState or contextValue changes
useIsomorphicLayoutEffect(() = > {
const { subscription } = contextValue
// Iterate over each node in the list, triggering a callback function that publishes notifications to the components that subscribe to the Store
// Then create a new list
subscription.trySubscribe()
// The value of state may change after the notification is published,
// Continue traversing the list, notification
if(previousState ! == store.getState()) { subscription.notifyNestedSubs() }return () = > {
// Cancel listening to facilitate garbage collection
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
// If we don't pass a context, we use ReactReduxContext by default
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
export default Provider
Copy the code
A Provider is a function that acts as a component. Internally, it performs several steps:
- to
store
I created asubscription
Object for conveniencestore
Update with helpsubscription
And assign two values toContextValue
; - use
Context.Provider
willContextValue
Passed to theChildren
.
If react-Redux uses context to pass stores, what does it do internally if it also supports custom context?
When using a Context in a Provider, create a Conetxt and pass it as a parameter to the Provider, but be sure to manually import the Context in the child components otherwise an error will be reported:
import React from 'react';
export const ColorContext = React.createContext({color: 'red'});
Copy the code
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
import { ColorContext } from './store/context';
ReactDOM.render(
<Provider store={store} context={ColorContext}>
<App />
</Provider >,
document.getElementById('root')
);
Copy the code
To avoid errors, in the child component,context is passed as props:
// App.js
<Count context={ColorContext} />
Copy the code
To read the ColorContext internally, we can retrieve it from the ownProps:
// Counter.js
function Counter(props){
const { count, increment, ColorContext } = props;
const contextValue = useContext(ColorContext);
console.log('contextValue123', contextValue);
return (
<>
<p>{count}</p>
<button onClick = {() = > increment(1)}>+</button>
</>)}function mapStateToProps(state, ownProps) {
return {
count: state.count,
ColorContext: ownProps.ownProps
}
}
function mapDispatchToProps(dispatch) {
return { increment: bindActionCreators(actionCreators.increment, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Copy the code
In addition, according to the back of the source code available, this will not report an error… , but cannot get the Context on ownProps
<Count store={store} />
Copy the code
connectAdvanced
This is a never-before-used API! It is the underlying implementation of Connect, but the official documentation says it may be removed from the code in the future. ConnectAdvanced differs from Connect in that it does not specify how state, props, and dispatches are passed into the final props, nor does it cache the resulting props to optimize performance.
ConnectAdvanced accepts two parameters: selectorFactory and factoryOptions. In selectorFactory we can implement what parameters are passed to the component. These parameters can depend on store.getState(), action, and the component’s own props. Official DEMO:
function selectorFactory(dispatch) {
let ownProps = {}
let result = {}
const actions = bindActionCreators(actionCreators, dispatch)
const addTodo = (text) = > actions.addTodo(ownProps.userId, text)
return (nextState, nextOwnProps) = > {
const todos = nextState.todos[nextOwnProps.userId]
constnextResult = { ... nextOwnProps, todos, addTodo } ownProps = nextOwnPropsif(! shallowEqual(result, nextResult)) result = nextResultreturn result
}
}
export default connectAdvanced(selectorFactory)(TodoApp)
Copy the code
Let’s just call the function returned by selectorFactory a selector. The selector function takes two arguments, the new state and the props of the component itself. The selector function is called when a component receives a new state or props, and then returns a simple object to the wrapped component.
There are several possible parameters in factoryOptions:
getDisplayName
: get beconnectAdvanced
The name of the new component to return after the package. Default isConnectAdvanced('+name+')
methodName
: used to display in error messages. The default isconnectAdvanced
;shouldHandleStateChanges
: Whether to trackstate
The default value istrue
;forwardRef
: When neededref
To save isconnectAdvanced
Set to when wrapping new components afterfalse
;context
:createContext
To create thecontext
;- The remaining parameters: will be passed to
selectorFactory
namelyselector
Method in the second argument.
Here on the basic structure of the source code, some of the parameters to be removed by me deleted!
export default function connectAdvanced(
selectorFactory,
{
getDisplayName = (name) => `ConnectAdvanced(${name}) `,
methodName = 'connectAdvanced',
shouldHandleStateChanges = true,
forwardRef = false,
context = ReactReduxContext,
//The rest of the arguments are passed to selectorFactory... connectOptions }){
// ...
return function wrapWithConnect(WrappedComponent) {
// ...
return hoistStatics(Connect, WrappedComponent)
}
}
Copy the code
hoistStatics comes from the package hoist- non-React-statics that copies static properties from your original component to the new component wrapped with connectAdvanced. Here’s an example:
const Component.staticMethod = () = > {}
const WrapperComponent = enhance(Component);
typeof WrapperComponent.staticMethod === undefined // true
// The solution is as follows
function enhance(Component) {
class WrapperComponent extends React.Component {/ *... * /}
WrapperComponent.staticMethod = Component.staticMethod;
return Enhance;
}
// If we don't know all the static property values of the original component, then the above method is invalid.
// Hoist non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(Component) {
Class WrapperComponent extends React.Component {/ *... * /}
hoistNonReactStatic(WrapperComponent, Component);
return WrapperComponent;
}
Copy the code
Continue with the analysis to understand each line of code inside:
export default function connectAdvanced(){
// The first half of this section reports an error for the attributes to be removed...
const Context = context; // Save the context to be passed inside the component
return function wrapWithConnect(WrappedComponent) {
// ...}}Copy the code
Go to the wrapWithConnect source code:
function wrapWithConnect(WrappedComponent) {
// Get the name of the component first
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
// Name of the new component returned after being wrapped => 'ConnectAdvanced(${name})'
const displayName = getDisplayName(wrappedComponentName)
constselectorFactoryOptions = { ... connectOptions, getDisplayName, methodName, shouldHandleStateChanges, displayName, wrappedComponentName, WrappedComponent, }const { pure } = connectOptions
// Create our selector function
function createChildSelector(store) {
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// Use useMemo to improve the performance of the new component returned by the package
const usePureOnlyMemo = pure ? useMemo : (callback) = > callback()
function ConnectFunction(props){}
// Use react. memo to improve the performance of the new component returned by the package
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
Connect.WrappedComponent = WrappedComponent
Connect.displayName = ConnectFunction.displayName = displayName
// ...
}
Copy the code
Here is a ConnectFunction function:
function ConnectFunction(props) {
// From props, we deconstruct the context, the ref, and the remaining props
const [
propsContext,
reactReduxForwardedRef,
wrapperProps,
] = useMemo(() = > {
const{ reactReduxForwardedRef, ... wrapperProps } = propsreturn [props.context, reactReduxForwardedRef, wrapperProps]
}, [props])
ReactReduxContext = ReactReduxContext; const Context = context
// Provider allows users to pass in a custom Context. If not, the default is ReactReduxContext
// If the user passed the context on the Provider, the user had to manually pass the context in the nested component, so we need to determine the value of the context.
// Use the appropriate Context
const ContextToUse = useMemo(() = > {
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)? propsContext : Context }, [propsContext, Context])// Get the value in the context
const contextValue = useContext(ContextToUse)
// The Provider is a Provider
// Provider encapsulates contextValue with store and subscription
// If the user passes a custom Context, the nested component must pass the Context or store to avoid an error.
// contextValue. Store: contextValue. Store: contextValue
// If there is no store, then we need to find the store in contextValue, if there is no store in contextValue
// An error is reported
// This is why if a user passes a context, it is necessary to pass a custom context or store in the nested component
const didStoreComeFromProps =
Boolean(props.store) &&
Boolean(props.store.getState) &&
Boolean(props.store.dispatch)
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
if( process.env.NODE_ENV ! = ='production'&&! didStoreComeFromProps && ! didStoreComeFromContext ) {throw new Error(
`Could not find "store" in the context of ` +
`"${displayName}". Either wrap the root component in a <Provider>, ` +
`or pass a custom React context provider to <Provider> and the corresponding ` +
`React context consumer to ${displayName} in connect options.`)}/ / save the store
const store = didStoreComeFromProps ? props.store : contextValue.store
// As mentioned earlier, the selector function will be called if the store changes
const childPropsSelector = useMemo(() = > {
return createChildSelector(store)
}, [store])
// ...
}
Copy the code
const [subscription, notifyNestedSubs] = useMemo(() = > {
// Return [null, null] if you don't need to listen for state changes.
if(! shouldHandleStateChanges)return NO_SUBSCRIPTION_ARRAY
/ / if the store is provided by the context, the need to contextValue. The subscription for listening
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
/ / copy notifyNestedSubs
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// Override the current contextValue
const overriddenContextValue = useMemo(() = > {
// If the store is from Props, then return contextValue directly
if (didStoreComeFromProps) {
return contextValue
}
// Otherwise, the component subscription is placed in the context
return {
...contextValue,
subscription,
}
}, [didStoreComeFromProps, contextValue, subscription])
Copy the code
// When the store is updated, it is necessary to force the component that is being updated to be updated, and therefore the child component to be updated as well
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
/ / will throw an exception when an error occurs, the normal previousStateUpdateResult should be null
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
// Here is the source code for each parameter
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]}const EMPTY_ARRAY = []
const initStateUpdates = () = > [null.0]
Copy the code
/ / mentioned earlier, useIsomorphicLayoutEffect internal judgment,
// Return useEffect if it is a server
// Return useLayoutEffect for the browser
import { useIsomorphicLayoutEffect } from '.. /utils/useIsomorphicLayoutEffect'
/ / here you can understand, call the useEffect/useLayoutEffect, but the parameter is dynamic
function useIsomorphicLayoutEffectWithArgs(effectFunc, effectArgs, dependencies) {
useIsomorphicLayoutEffect(() = >effectFunc(... effectArgs), dependencies) }// Continue analyzing the source code
const lastChildProps = useRef() // Pass the props (Component) for the current Component before the update.
const lastWrapperProps = useRef(wrapperProps) // Update the props (ConnectComponent) for the new component
const childPropsFromStoreUpdate = useRef() // Determine if the component update is due to a store update
const renderIsScheduled = useRef(false)
// usePureOnlyMemo Determines whether to use memo based on pure
const actualChildProps = usePureOnlyMemo(() = > {
// View updates may be due to store updates that cause the Component to get new props
// If the Component has new props and the ConnectComponent has the same props, then we should use the new props so that we can get the new props for ConnectComponent
// But if we have a new ConnectComponent props, which might change the Component props, then the calculation will be recalculated
// To avoid problems, we will update the Component with new props only if ConnectComponent's props are the same
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
// Call the selector function
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// We need the following function to perform the synchronization to render, but using useLayoutEffect in SSR will cause an error, we need to check this, if SSR, then use effect instead to avoid this warning
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs,
])
function captureWrapperProps(lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs) {
// For the next comparison
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false
// Clear the reference and update it
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
}
// When the store or subscription changes, we resubscribe
useIsomorphicLayoutEffectWithArgs(
subscribeUpdates,
[
shouldHandleStateChanges,
store,
subscription,
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
childPropsFromStoreUpdate,
notifyNestedSubs,
forceComponentUpdateDispatch,
],
[store, subscription, childPropsSelector]
)
Copy the code
function subscribeUpdates(
shouldHandleStateChanges, // Whether to subscribe to store updates
store, // store
subscription, // subscription
childPropsSelector, // selector
lastWrapperProps, // Props for the last component
lastChildProps, // The last props
renderIsScheduled, // Whether scheduling is underway
childPropsFromStoreUpdate // Check whether the subprops are an update from the store
notifyNestedSubs, // notifyNestedSubs
forceComponentUpdateDispatch
) {
// If there is no subscription, return directly
if(! shouldHandleStateChanges)return
// Capture the value and check if and when the component is unloaded
let didUnsubscribe = false
let lastThrownError = null
// This function is called every time a store update is propagated to this component
const checkForUpdates = () = > {
if (didUnsubscribe) {
// If you have unsubscribed, return directly
return
}
// Get the store before the update
const latestStoreState = store.getState()
let newChildProps, error
try {
// Call the selector to get the latest props
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}
if(! error) { lastThrownError =null
}
// If the old props are the same as the old props, don't do anything about it
if (newChildProps === lastChildProps.current) {
if(! renderIsScheduled.current) {/ / call the subscription. NotifyNestedSubs ()
notifyNestedSubs()
}
} else {
// use useState/useReducer to trace the latest props.
// There will be no way to determine whether the value has been processed
// There is no way to clear this value to trigger a forced render
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// If the child props were updated, then it is possible to render again
forceComponentUpdateDispatch({
type: 'STORE_UPDATED'.payload: {
error,
},
})
}
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
// Update the view to the changed values in the store
checkForUpdates()
const unsubscribeWrapper = () = > {
didUnsubscribe = true
subscription.tryUnsubscribe()
subscription.onStateChange = null
if (lastThrownError) {
// When the store is updated but a component does not depend on the updated state in the store, there is a problem with mapState
// An error is reported
throw lastThrownError
}
}
return unsubscribeWrapper
}
Copy the code
// Back in the main source code, we have processed the parameters that need to be passed, and the next step is to render the component
// useMemo is also used to optimize performance
const renderedWrappedComponent = useMemo(
() = > (
<WrappedComponent
{. actualChildProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
// If React sees the exact same element reference as last time, it will exit and re-render the child elements, just as it would if it were wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() = > {
if (shouldHandleStateChanges) {
// If you need to update the component based on store changes, use context.provider to wrap the component
// And pass in the processed value
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>)}// Otherwise, return the original component
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
Copy the code
Here the connectAdvanced source code is analyzed. Let’s get this straight. I’ll write a line of code for the sake of illustration:
function selector = (dispatch, ... args) = >{}
const connectComponent = connectAdvanced(selector, options)(component)
Copy the code
-
ConnectAdvanced is a function that takes a selector and options object:
selector
The method is user defined, receive onedispacth
Parameter, and finally you need to return one with an incoming parameterNextState and nextOwnProps
This inner function eventually returns an object whose key-value pair is to be passed tocomponent
The component’sprops
;options
Object, either official or custom, is passed in. If it is custom, it is passed inselector
In the second argument of. There are several important parameters that are officially specified, among whichpure
The value of determines whether the last returned component should be optimized for performance;connectAdvanced
The result is a higher-order function that takes the component to wrap.
-
The options value is then checked and a warning is given to properties that are about to be removed.
-
Save the context in options;
-
Return the higher-order function wrapWithConnect(WrappedComponent), taking the component to wrap. The next step is to analyze the contents of wrapWithConnect(WrappedComponent).
-
First check the incoming component to determine whether it is a qualified component;
-
Record the name of the wrapped component, and then name the new component returned, connectComponent.
-
Encapsulate the extra arguments in options with the defined arguments as the selector’s arguments.
-
According to the options parameter pure, use the usePureOnlyMemo variable to determine whether to use the useMemo method.
-
Create a Connect component with ConnectFunction and set WrappedComponent and displayName for the component. ConnectFunction is the core of connectAdvanced:
ConnectFunction
Take a parameterprops
theprops
You know, after the packageConnetComponent
The parameters of theprops
;- will
Context, ref, and the rest of the parameters
fromprops
Is decoupled from; - Get the final
context
And get out of itcontextValue
; - To obtain
store
; - To define a
memo
Function, whenstore
The user-defined one is invoked if the value ofselector
Functions; - Get the final
ContextValue
The value of the; - To eventually return the wrapped component
actualChildProps
: - Used when the value of a dependency changes
useEffect
oruseLayoutEffect
To invoke thecaptureWrapperProps
, which will be currentconnentComponent
theprops
andstore
thestate
Record it so you can compare it to the next rendering ifstore
The value of thenotifyNestedSubs
Fires each callback function; - when
store
orsubscription
When an update is made, a notification is initiated,subscribeUpdates
; - The last judgment
shouldHandleStateChanges
To decide whether or not to parcel it outsideContext
If not, return it directlyConnect
Components.
-
After the Connect component is obtained, we need to determine the forwardRef. If it exists, we need to use the React. ForwardRef to forward the Connect; otherwise, we need to use Connect directly.
summary
ConnectAdvanced returns a Context.Comsumer with props for the last component passed in. The main difference between connectAdvanced and connect is that the internal pure property defaults to false. That is, it does not start performance tuning.
If react-Redux uses context to pass stores, what is done internally to support custom context as well?
React-redux uses a Context to store and a subscription to publish the store, and when you want to use a Context with react-Redux, Remember to insert the same name of context into the component as props, so connectAdvanced will incorporate the user-defined context as props into the final component’s props.
So while react-Redux allows you to make a global context, it’s not necessary
shallowEqual
Before exploring the connect source code, learn about shallowEqual:
value | = = = | is(x,y) |
---|---|---|
NaN, NaN | false | true |
0, + 0 | true | true |
0、-0 | true | false |
+ 0, 0 | true | false |
. / / the Object is to basic data types: null, and undefined, number, string, Boolean make very precise comparison,
// However, there is no direct comparison for reference data types.
function is(x, y) {
if (x === y) {
// Handle 0, +0, and -0 cases
returnx ! = =0|| y ! = =0 || 1 / x === 1 / y
} else {
// Handle NaN cases
returnx ! == x && y ! == y } }export default function shallowEqual(objA, objB) {
// Use is to determine whether two variables are equal
if (is(objA, objB)) return true
if (
typeofobjA ! = ='object' ||
objA === null ||
typeofobjB ! = ='object' ||
objB === null
) {
return false
}
// Iterate over the key-value pairs of the two objects, comparing them once
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if(keysA.length ! == keysB.length)return false
for (let i = 0; i < keysA.length; i++) {
if (
/ / Object. The prototype. The hasOwnProperty and operator is different, in this method will ignore those attributes inherited from the prototype chain.
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]]) ) {return false}}return true
}
Copy the code
The first comparison is called to determine whether the two values are the same. Some common basic letters === are used to determine if there is a problem; Then check whether it is not an object or null. If so, return false. Otherwise, iterate over the keys of the two objects and compare them. As you can see from the source code, shallow comparisons are not applicable to nested type comparisons.
That’s the idea of a shallow comparison
As you can see from MDN, IS is a Polyfill implementation of object.is ().
connect
Look at the source code by the way to see the official document, the first feeling is my English really progress! The second feeling is that I did not flip before, how to see a lot of negligence? !
Basic architecture
// src/connect/connect.js
import connectAdvanced from '.. /components/connectAdvanced'
import shallowEqual from '.. /utils/shallowEqual'
import defaultMapDispatchToPropsFactories from './mapDispatchToProps'
import defaultMapStateToPropsFactories from './mapStateToProps'
import defaultMergePropsFactories from './mergeProps'
import defaultSelectorFactory from './selectorFactory'
export function createConnect({ onnectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory, } = {}){
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ... extraOptions } = {}){
// ...
return connectHOC()
}
}
export default /*#__PURE__*/ createConnect()
Copy the code
The createConnect method is executed before the export, and it returns a familiar connect function, with the internal HOC function called. It involves a lot of secret parameters with similar names, which makes it a little painful to see.
When createConnect() is called, no parameters are passed, so the parameters are read by default:
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,
Copy the code
We then return the connect function of our property, which supports four arguments: mapStateToProps, mapDispatch, mergeProps, and an object. According to the official document:
-
MapStateToProps: Functions that can be passed in store.getState() and the props of the current component itself.
-
MapDispacthToProps: functions that receive props from store.dispacth and the current component itself. Finally, an object needs to be returned.
-
MergeProps: functions that receive the props of store.getState(), store.dispacth, and the component itself.
-
Options: This object has several values:
{ context? :Object.// When we introduce the Provider into the context, we can write it herepure? : boolean,// In order to improve react-Redux performance, pureComponent is enabled by default and will only rerender the component if the result of its props, mapStateToProps, or mapDispatchToProps changesareStatesEqual? :Function.// The comparison function for state updates passed in when pure is enabledareOwnPropsEqual? :Function.// This is the comparison function of the props update when pure is enabledareStatePropsEqual? :Function.// Comparison function when mapStateToProps is updated when pure is enabledareMergedPropsEqual? :Function.// The comparison function when the return value of mergeProps is updated when pure is enabledforwardRef? : boolean,// If the component needs to receive a ref, set this to true } Copy the code
Finally, it returns a higher-order function. ConnectHOC is just like connectAdvanced, which takes two arguments, a selector function and an options object:
return connectHOC(selectorFactory, {
// To use when an error is reported
methodName: 'connect'.// Give the new component a name based on the name of the component wrapped in connect
getDisplayName: (name) = > `Connect(${name}) `.// If mapStateToProps is Null, then there is no need to listen for updates to state
shouldHandleStateChanges: Boolean(mapStateToProps),
// The following values are given to selectorFactory
// The second parameter of connectAdvanced, options, is an object
// All properties except the specified key will eventually become the second argument to the selector function
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
// any extra options args can override defaults of connect or connectAdvanced. extraOptions, })Copy the code
Step by step to understand the default parameters in createConnect:
connectHoc
Its default value is connectAdvanced, which I’ve already seen above, so I won’t repeat.
defaultMapStateToPropsFactories
To get started, take a look at the file wrapMapToProps introduced in this source code:
import verifyPlainObject from '.. /utils/verifyPlainObject'
export function wrapMapToPropsConstant(getConstant) {
return function initConstantSelector(dispatch, options) {
const constant = getConstant(dispatch, options)
function constantSelector() {
return constant
}
// dependsOnOwnProps determines whether the current mapStateToProps is dependent on the ownProps
constantSelector.dependsOnOwnProps = false
return constantSelector
}
}
// Do not rely on ownProps
export function getDependsOnOwnProps(mapToProps) {
returnmapToProps.dependsOnOwnProps ! = =null&& mapToProps.dependsOnOwnProps ! = =undefined
? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length ! = =1
}
// Encapsulate the mapToProps function
// Check whether selectorFactory is updated according to ownProps.
export function wrapMapToPropsFunc(mapToProps, methodName) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
// Allow detectFactoryAndVerify to get its props
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
proxy.mapToProps = mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
let props = proxy(stateOrDispatch, ownProps)
// props returns the function, handles the mapToProps, and uses the new function as a true mapToProps for subsequent calls
if (typeof props === 'function') {
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
// If it is not a normal object, a warning is issued
if(process.env.NODE_ENV ! = ='production')
verifyPlainObject(props, displayName, methodName)
return props
}
return proxy
}
}
Copy the code
// src/connect/mapStateToProps.js
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapStateToPropsIsFunction(mapStateToProps) {
return typeof mapStateToProps === 'function'
? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
: undefined
}
export function whenMapStateToPropsIsMissing(mapStateToProps) {
return! mapStateToProps ? wrapMapToPropsConstant(() = > ({})) : undefined
}
export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
Copy the code
MapStateToProps returns an array, respectively is whenMapStateToPropsIsFunction and whenMapStateToPropsIsMissing, from the function name, when mapStateToProps function or empty, The two functions are called separately.
defaultMapDispatchToPropsFactories
// src/connect/mapDispatchToProps.js
import bindActionCreators from '.. /utils/bindActionCreators'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
return typeof mapDispatchToProps === 'function'
? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
: undefined
}
export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
return! mapDispatchToProps ? wrapMapToPropsConstant((dispatch) = > ({ dispatch }))
: undefined
}
export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
return mapDispatchToProps && typeof mapDispatchToProps === 'object'
? wrapMapToPropsConstant((dispatch) = >
bindActionCreators(mapDispatchToProps, dispatch)
)
: undefined
}
export default [
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject,
]
Copy the code
MapDispatchToProps returns an array, Is whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing and whenMapDispatchToPropsIsObject respectively, from the function name, MapDispatchToProps is called when mapDispatchToProps is a function, empty, or object.
defaultMergePropsFactories
// src/connect/mergeProps.js
import verifyPlainObject from '.. /utils/verifyPlainObject'
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return{... ownProps, ... stateProps, ... dispatchProps } }export function wrapMergePropsFunc(mergeProps) {
return function initMergePropsProxy(dispatch, { displayName, pure, areMergedPropsEqual }) {
let hasRunOnce = false
let mergedProps
return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)
if (hasRunOnce) {
if(! pure || ! areMergedPropsEqual(nextMergedProps, mergedProps)) mergedProps = nextMergedProps }else {
hasRunOnce = true
mergedProps = nextMergedProps
if(process.env.NODE_ENV ! = ='production')
verifyPlainObject(mergedProps, displayName, 'mergeProps')}return mergedProps
}
}
}
export function whenMergePropsIsFunction(mergeProps) {
return typeof mergeProps === 'function'
? wrapMergePropsFunc(mergeProps)
: undefined
}
export function whenMergePropsIsOmitted(mergeProps) {
return! mergeProps ?() = > defaultMergeProps : undefined
}
export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
Copy the code
MergeProps returns an array of mergeProps whenMergePropsIsFunction and whenMergePropsIsOmitted, which are called whenMergePropsIsFunction is a function or is omitted.
defaultSelectorFactory
Select the factory function to generate the selector function required for connectAdvanced. The selector’s first argument is Dispatch and its second argument is an object.
// import defaultSelectorFactory from './selectorFactory'
// selectorFactory.js
export default function finalPropsSelectorFactory(dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ... options }) {
// Initialize the value first
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
if(process.env.NODE_ENV ! = ='production') {
// ...
}
// Use different functions based on Pure and call them
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
Copy the code
impureFinalPropsSelectorFactory
If we haven’t turned on pure, we can return mergeProps without doing anything, and print the props we want to pass to the wrapped component:
export function impureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch) {
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(
mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
Copy the code
pureFinalPropsSelectorFactory
When this function is executed for the first time, it uses the stored state and props to obtain the combined props passed to the package component.
If so, call handleSubsequentCalls to compare whether this update updates only state, props only, or both, then call different logic, and finally give the wrapped component the final props.
The following logic is related to the performance of react-Redux. MapStateToProps, mapDispatchtoProps, mergeProps, and Options can be passed to connect. The first two are required.
mapStateToProps
You can pass in two arguments,state
andownProps
, and finally return a new onestate
So we can know the newstate
Not only depends onstore
And also depends onownProps
If you update one of them, it will be recalculatedstate
;mapDispatchtoProps
It also supports passing two arguments,dispatch
andownProps
To return a newdispatch
The newdispatch
Depends ondispacth
andownProps
;mergeProps
It’s a function, and by default we’re going to omit it, so it’s going to be assigned by default, and then the newstate
, newdispacth
andownProps
Into a newprops
Passed to theConnect
Components.
The react-Redux function has been optimized to improve the performance of new components that are returned by Connect, both by the store and by the component itself props.
Connect has one more pure argument than connectAdvanced. The default is True. Is used to enable performance tuning.
export function pureFinalPropsSelectorFactory(mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }) {
let hasRunAtLeastOnce = false
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
// For the first execution, obtain the final stateProps and dispatchProps based on the current state and props
// Then retrieve the final component props from the same mergedProps
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
// Call this function if both its Props and store have changed
function handleNewPropsAndNewState() {
// Get the new stateProps directly from mapStateToProps
stateProps = mapStateToProps(state, ownProps)
// If the mapDispatchToProps method depends on the ownProps method, then a new dispatchProps method must be obtained
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// Pass the new stateProps, dispatchProps, and mergedProps to get the props of the final component
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// If only your props change
function handleNewProps() {
// If the ownProps parameter is used when declaring mapStateToProps, a new stateProps will be generated.
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// If the second parameter (ownProps) is used when declaring mapDispatchToProps, this parameter will be set to true
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// Only store changes
function handleNewState() {
// When the store changes, mapStateToProps returns the new value
const nextStateProps = mapStateToProps(state, ownProps)
// Determine whether the new stateProps are the same as the last stateProps
conststatePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps) stateProps = nextStateProps// If there is a change, the final props need to be recalculated
if (statePropsChanged)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// If it is not the first execution,
function handleSubsequentCalls(nextState, nextOwnProps) {
// Determine whether the Props of the component itself have changed
constpropsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)// Determine whether the store has changed
conststateChanged = ! areStatesEqual(nextState, state)// Record the current state and ownProps for comparison in the next update
state = nextState
ownProps = nextOwnProps
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// Call handleSubsequentCalls or handleFirstCall to determine if it was executed once
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
Copy the code
With selectorFactory in mind, the next step is the execution of connectAdvanced, which I won’t repeat.
Hook
Finally, let’s talk about the newly added hooks. In June, the project was still eating the old money. It was clear that the project used hooks, but about React-Redux, I was completely unaware of the existence of hooks.
useSelector
UseSelector is a bit like mapStateToProps. We can use the props to get the state we want from the store, but the difference is that mapStateToProps uses a shallow comparison, while useSelector uses a deep comparison. Whenever the state changes, UseSelector returns only one value. If you want to return an object, you are advised to use shallowEqual or create a memory selector with reselect that returns multiple values in one object. But only if one of the values changes returns a new object.
import { shallowEqual, useSelector } from 'react-redux'
import { createSelector } from 'reselect'
// Regular usage
export const CounterComponent = () = > {
const counter = useSelector((state) = > state.counter)
return <div>{counter}</div>
}
// Use a shallow comparison to get an object
export const ObjComponent = () = > {
const obj = useSelector((state) = > state.obj, shallowEqual)
return <div>{obj.counter}</div>
}
/ / reselect
const selectNumCompletedTodos = createSelector(
(state) = > state.todos,
(todos) = > todos.filter((todo) = > todo.completed).length
)
export const CompletedTodosCounter = () = > {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}
export const App = () = > {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>)}Copy the code
Next, start analyzing the source code:
// useSelector is returned by createSelectorHook
export const useSelector = /*#__PURE__*/ createSelectorHook()
Copy the code
So we can infer that
createSelectorHook() = (selector, equalityFn) = > selectedState
Copy the code
Let’s see createSelectorHook:
export function createSelectorHook(context = ReactReduxContext) {
CreateSelectorHook () does not pass parameters, so context = ReactReduxContext
const useReduxContext =
/ / get the context
context === ReactReduxContext
? useDefaultReduxContext
: () = > useContext(context)
// Return a function (selector, equalityFn = refEquality) => selectorState to useSelector
// const refEquality = (a, b) => a === b useSelector
return function useSelector(selector, equalityFn = refEquality) {
// Check the selector, equalityFn
if(process.env.NODE_ENV ! = ='production') {
if(! selector) {throw new Error(`You must pass a selector to useSelector`)}if (typeofselector ! = ='function') {
throw new Error(`You must pass a function as a selector to useSelector`)}if (typeofequalityFn ! = ='function') {
throw new Error(
`You must pass a function as an equality function to useSelector`)}}// As we saw earlier in the source code, the context will store stores and subscription
const { store, subscription: contextSub } = useReduxContext()
// Get selectedState and return it
const selectedState = useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
)
useDebugValue(selectedState)
return selectedState
}
}
Copy the code
To look at how useSelectorWithStoreAndSubscription selectState return we want:
function useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub // subscription
) {
const [, forceRender] = useReducer((s) = > s + 1.0)
// Create a new subscription object
const subscription = useMemo(() = > new Subscription(store, contextSub), [
store,
contextSub,
])
const latestSubscriptionCallbackError = useRef()
const latestSelector = useRef()
const latestStoreState = useRef()
const latestSelectedState = useRef()
// Get the state in the current store
const storeState = store.getState()
let selectedState
try {
// If the selector changes, or the state in the store changes, or an error occurs
// Then re-execute selector to get the new selectState
// Otherwise, return the last selectState
if( selector ! == latestSelector.current || storeState ! == latestStoreState.current || latestSubscriptionCallbackError.current ) {// Compare new selectState to last selectState
// If so, return the last selectState
// Otherwise, return new selectState
const newSelectedState = selector(storeState)
if (
latestSelectedState.current === undefined| |! equalityFn(newSelectedState, latestSelectedState.current) ) { selectedState = newSelectedState }else {
selectedState = latestSelectedState.current
}
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current) {
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}
throw err
}
// This function saves the selector, store state, selectState, and subscription capture errors each time it is executed
// For the next comparison
useIsomorphicLayoutEffect(() = > {
latestSelector.current = selector
latestStoreState.current = storeState
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
})
// When the store or subscription changes
// Call checkForUpdates to get the new storeState and selectState
// Compare the value of selectState to the value of selectState last time
/ / if, update latestSelectedState and latestStoreState latestSubscriptionCallbackError
useIsomorphicLayoutEffect(() = > {
function checkForUpdates() {
try {
const newStoreState = store.getState()
const newSelectedState = latestSelector.current(newStoreState)
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
latestStoreState.current = newStoreState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}
forceRender()
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () = > subscription.tryUnsubscribe()
}, [store, subscription])
// Finally return the selectedState we need
return selectedState
}
Copy the code
Mainly through useSelector internal useSelectorWithStoreAndSubscription selectState to get what we want, UseSelectorWithStoreAndSubscription by comparing the selector and store to judge whether or not to implement the selector to get the latest selectState, and then capturing the new selectState selectState last time To determine which value to return. The current selector, store, selectState, and captured errors are recorded internally for comparison purposes. The purpose is to improve the performance of the useSelector without having to return new content every time the data is the same as before and cause the view to update.
useDispatch
The essence of useDiapatch is to get dispatches in a store. Here’s an official chestnut:
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) = > {
const dispatch = useDispatch()
const increaseCounter = useCallback(() = > dispatch({ type: 'increase-counter' }), [])
return (
<div>
<span>{value}</span>
<button onClick={increaseCounter}>Increase counter</button>
</div>)}Copy the code
Look directly at the source:
export function createDispatchHook(context = ReactReduxContext) {
const useStore =
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
return function useDispatch() {
const store = useStore()
return store.dispatch
}
}
export const useDispatch = /*#__PURE__*/ createDispatchHook()
Copy the code
It makes sense to first get the Context, then get the store in the Context, and then return to Store.Dispatch.
useStore
In useDispatch, there is a code called createStoreHook(context), which is actually an implementation of useStore:
export function createStoreHook(context = ReactReduxContext) {
const useReduxContext =
context === ReactReduxContext
? useDefaultReduxContext
: () = > useContext(context)
return function useStore() {
const { store } = useReduxContext()
return store
}
}
export const useStore = /*#__PURE__*/ createStoreHook()
Copy the code
First get the Context, then get the store in the Context, and finally return the Store ~
summary
Source intermittently looked at a week, but also looked back and forth several times, a little around. The website says that connectAdvanced will be deprecation in the near future, but we still need to work hard to implement Connect. MapStateToProps () and mapDispacthToProps () can use the ownProps () as the second parameter of the useMemo hook (). I also came into contact with the resELECT library.
Finally, be sure to read more English documents
reference
- The react – the story’s official website
- React-redux (1): connectAdvanced
- Polyfill
- Do you really understand shallow comparison?
If there are any mistakes, please point them out, thanks for reading ~