Interpretation of react-redux source code


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.

Guess you like

Origin blog.51cto.com/15080030/2592683