The thing written in the previous react-redux as glue seems unnecessary, but in fact, as the connection between the data layer (redux) and the UI layer (react), the implementation details have a decisive impact on the overall performance. The cost of random update of the component tree is much higher than the cost of running the reducer tree several times, so it is necessary to understand its implementation details
One of the benefits of carefully understanding react-redux is that you can have a basic understanding of performance. Consider a question:
What is the performance impact of this line of code in a corner of the dispatch({type:'UPDATE_MY_DATA', payload: myData}) component tree? Several sub-problems:
1. Which reducers have been recalculated?
2. From which component does the triggered view update start?
3. Which component's render is called?
4. Has every leaf component been affected by diff? why?
If you can’t answer these questions accurately, you will definitely have no idea about performance.
1. Function
First of all, it is clear that redux is only a data layer, and react is only a UI layer, there is no connection between the two
If the left and right hands hold redux and react respectively, then the actual situation should be like this:
Redux has set the data structure (state) and the calculation method (reducer) of each field.
React renders the initial page according to the view description (Component)
It might look like this:
redux | react
myUniversalState | myGreatUI
human | noOneIsHere
soldier |
arm |
littleGirl |
toy |
ape | noOneIsHere
hoho |
tree | someTrees
mountain | someMountains
snow | flyingSnow
There are everything in redux on the left, but React doesn’t know, only the default elements are displayed (no data), there are some component partial states and scattered props, the page is like a static picture, and the component tree looks just like some pipes. Connected big shelf
Now we consider adding react-redux, then it will become like this:
react-redux
redux -+- react
myUniversalState | myGreatUI
HumanContainer
human -+- humans
soldier | soldiers
ArmContainer
arm -+- arm
littleGirl | littleGirl
toy | toy
ApeContainer
ape -+- apes
hoho | hoho
SceneContainer
tree -+- Scene
mountain | someTrees
snow | someMountains
flyingSnow
Note that Arm interaction is more complicated and not suitable for control by the upper layer (HumanContainer), so there is a nested Container
Container hands the state in redux to react, so that the initial data is available, so what if the view needs to be updated?
Arm.dispatch({type:'FIRST_BLOOD', payload: warData})
Someone fired the first shot, causing the soldier to hang a (state change), then these parts must change:
react-redux
redux -+- react
myNewUniversalState | myUpdatedGreatUI
HumanContainer
human -+- humans
soldier | soldiers
| diedSoldier
ArmContainer
arm -+- arm
| inactiveArm
A dead soldier and a dropped arm (update view) appear on the page, and everything else is fine (ape, scene)
The above description is the role of react-redux:
Pass state from redux to react
And is responsible for updating react view after redux state change
Then you guessed it, the implementation is divided into 3 parts:
Add a small water source to the big shelf connected by the pipeline (inject state as props into the lower view through the Container)
Let the small water source water (monitor for state change, update the view below through the setState of the Container)
No small water source, don’t rush (built-in performance optimization, compare the cached state and props to see if it is necessary to update)
2. The key to achieve the
key parts of the source code are as follows:
// from: src/components/connectAdvanced/Connect.onStateChange
onStateChange() {
// state change时重新计算props
this.selector.run(this.props)
// 当前组件不用更新的话,通知下方container检查更新
// 要更新的话,setState空对象强制更新,延后通知到didUpdate
if (!this.selector.shouldComponentUpdate) {
this.notifyNestedSubs()
} else {
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
// 通知Container下方的view更新
//!!! 这里是把redux与react连接起来的关键
this.setState(dummyState)
}
}
The most important setState is here. The secret of view update after dispatch action is this:
1.dispatch action
2.redux计算reducer得到newState
3.redux触发state change(调用之前通过store.subscribe注册的state变化监听器)
4.react-redux顶层Container的onStateChange触发
1.重新计算props
2.比较新值和缓存值,看props变了没,要不要更新
3.要的话通过setState({})强制react更新
4.通知下方的subscription,触发下方关注state change的Container的onStateChange,检查是否需要更新view
In step 3, the action of react-redux registering store change monitoring with redux occurs when connect()(myComponent). In fact, react-redux only directly monitors the state change of redux for the top-level Container, and the lower-level Containers all transmit notifications internally. , As follows:
// from: src/utils/Subscription/Subscription.trySubscribe
trySubscribe() {
if (!this.unsubscribe) {
// 没有父级观察者的话,直接监听store change
// 有的话,添到父级下面,由父级传递变化
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
}
}
Here we do not directly monitor the state change of redux, instead of maintaining the state change listener of the Container by ourselves, in order to achieve controllable order, such as mentioned above:
// 要更新的话,延后通知到didUpdate
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
This ensures that the trigger sequence of the listener is in accordance with the hierarchy of the component tree. The big subtree is notified first, and after the big subtree is updated, the small subtree is notified to update.
The whole update process is like this. As for the step of "injecting state as props into the lower view through Container", there is nothing to say, as follows:
// from: src/components/connectAdvanced/Connect.render
render() {
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
Create a props based on the state field required by WrappedComponent and inject it into it through React.createElement. When ContainerInstance.setState({}), the render function is called again, new props are injected into the view, the view will receive props... the view update will really begin
3. Techniques to
make pure functions have state
function makeSelectorStateful(sourceSelector, store) {
// wrap the selector in an object that tracks its results between runs.
const selector = {
run: function runComponentSelector(props) {
try {
const nextProps = sourceSelector(store.getState(), props)
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}
return selector
}
Wrap a pure function with an object to have a local state, which is similar to the new Class Instance. In this way, the pure part is separated from the impure part. The pure is still pure, and the impure is outside. The class is not as clean as this
Default parameters and object deconstruction
function connectAdvanced(
selectorFactory,
// options object:
{
getDisplayName = name => `ConnectAdvanced(${name})`,
methodName = 'connectAdvanced',
renderCountProp = undefined,
shouldHandleStateChanges = true,
storeKey = 'store',
withRef = false,
// additional options are passed through to the selectorFactory
...connectOptions
} = {}
) {
const selectorFactoryOptions = {
// 展开 还原回去
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
withRef,
displayName,
wrappedComponentName,
WrappedComponent
}
}
Can be simplified to this:
function f({a = 'a', b = 'b', ...others} = {}) {
console.log(a, b, others);
const newOpts = {
...others,
a,
b,
s: 's'
};
console.log(newOpts);
}
// test
f({a: 1, c: 2, f: 0});
// 输出
// 1 "b" {c: 2, f: 0}
// {c: 2, f: 0, a: 1, b: "b", s: "s"}
Here are 3 es6+ tips:
Default parameters. Prevent undefined error on the right side during deconstruction
Object deconstruction. Wrap the remaining attributes into the others object
Expand operator. Expand others and merge attributes to the target object
The default parameters are es6 features, there is nothing to say. Object deconstruction is the Stage 3 proposal,...others is its basic usage. The expansion operator expands the object and merges it to the target object, which is not complicated
What's more interesting is that the combination of object deconstruction and expansion operators is used here to realize this scenario where parameters need to be packaged and restored. If these two features are not used, you may need to do this:
function connectAdvanced(
selectorFactory,
connectOpts,
otherOpts
) {
const selectorFactoryOptions = extend({},
otherOpts,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
withRef,
displayName,
wrappedComponentName,
WrappedComponent
)
}
You need to clearly distinguish between connectOpts and otherOpts. It will be more troublesome to implement. If you combine these techniques, the code is quite concise
There is also an es6+ tip:
addExtraProps(props) {
//! 技巧 浅拷贝保证最少知识
//! 浅拷贝props,不把别人不需要的东西传递出去,否则影响GC
const withExtras = { ...props }
}
One more reference means one more risk of memory leaks, and should not be given to what is not needed (minimal knowledge)
Parameter pattern matching
function match(arg, factories, name) {
for (let i = factories.length - 1; i >= 0; i--) {
const result = factories[i](arg)
if (result) return result
}
return (dispatch, options) => {
throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`)
}
}
The factories are like this:
// mapDispatchToProps
[
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject
]
// mapStateToProps
[
whenMapStateToPropsIsFunction,
whenMapStateToPropsIsMissing
]
Establish a series of case functions for various conditions of the parameters, and then let the parameters flow through all cases in turn, return the result if any one is matched, and enter the error case if there is no match
Similar to switch-case, it is used to perform pattern matching on parameters, so that various cases are decomposed, and their respective responsibilities are clear (the naming of each case function is very accurate)
Lazy parameters
function wrapMapToPropsFunc() {
// 猜完立即算一遍props
let props = proxy(stateOrDispatch, ownProps)
// mapToProps支持返回function,再猜一次
if (typeof props === 'function') {
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
}
Among them, lazy parameters refer to:
// 把返回值作为参数,再算一遍props
if (typeof props === 'function') {
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
This implementation is related to the scenario faced by react-redux. The return function is mainly to support fine-grained mapToProps control at the component instance level (the default is component level). In this way, different mapToProps can be given to different component instances to support further performance improvement
From the implementation point of view, it is equivalent to delaying the actual parameters, and supports passing in a parameter factory as a parameter. The external environment is passed to the factory for the first time, and the factory then creates the actual parameters according to the environment. With the addition of the factory link, the control granularity is refined (the component level is refined to the component instance level, and the external environment is the component instance information)
PS For related discussions on lazy parameters, see https://github.com/reactjs/react-redux/pull/279
Four. Questions
1. Where did the default props.dispatch come from?
connect()(MyComponent)
Without passing any parameters to connect, the MyComponent instance can also get a prop called dispatch. Where did it secretly hang?
function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
return (!mapDispatchToProps)
// 就是这里挂上去的,没传mapDispatchToProps的话,默认把dispatch挂到props上
? wrapMapToPropsConstant(dispatch => ({ dispatch }))
: undefined
}
A mapDispatchToProps = dispatch => ({ dispatch }) is built in by default, so there is dispatch on component props. If mapDispatchToProps is specified, it will not hang
2. Will multi-level Container face performance problems?
Consider this scenario:
App
HomeContainer
HomePage
HomePageHeader
UserContainer
UserPanel
LoginContainer
LoginButton
When nested containers appear, will the view update be repeated many times when the state concerned by HomeContainer changes? such as:
HomeContainer update-didUpdate
UserContainer update-didUpdate
LoginContainer update-didUpdate
If this is the case, a light dispatch will cause the 3 subtrees to be updated, and it feels like the performance is about to explode.
This is not the case. For multi-level Containers, the situation of going twice does exist, but going twice here does not refer to view update, but to state change notification
The upper container will notify the lower container to check for updates after didUpdate, and may go through the small subtree again. But in the process of updating the big subtree, when it goes to the lower Container, the little subtree starts to update at this time. The notification after the big subtree didUpdate will only let the lower container go through the check without actual updates.
The specific cost of the inspection is to compare state and props with === comparison and shallow reference comparison (also === comparison first), and it is over if there is no change, so the performance cost of each lower-level Container is two === Comparison, it doesn't matter. In other words, don’t worry about the performance overhead of using nested Containers
5. Source code analysis
Github address: https://github.com/ayqy/react-redux-5.0.6
PS comments are still sufficiently detailed.