React-Redux source code analysis

Welcome to my personal blog

foreword

The react-redux library provides Providercomponents to inject the store into the application through context, and then you can use connecthigher-order methods to obtain and monitor the store, and then calculate the new props according to the store state and the component's own props, inject the component, and listen to the store to compare The calculated new props determine whether the component needs to be updated.

React and redux application structure

Provider

First, the react-redux library provides Providercomponents to inject the store into an entry component of the entire React application, usually the top-level component of the application. ProviderThe component uses the context to pass down the store:

// 内部组件获取redux store的键
const storeKey = 'store'
// 内部组件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
  // 声明context,注入store和可选的发布订阅对象
  getChildContext() {
    return { [storeKey]: this[storeKey], [subscriptionKey]: null }
  }

  constructor(props, context) {
    super(props, context)
    // 缓存store
    this[storeKey] = props.store;
  }

  render() {
    // 渲染输出内容
    return Children.only(this.props.children)
  }
}

Example

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './components/App'
import reducers from './reducers'

// 创建store
const store = createStore(todoApp, reducers)

// 传递store作为props给Provider组件;
// Provider将使用context方式向下传递store
// App组件是我们的应用顶层组件
render(
  <Provider store={store}>
    <App/>
  </Provider>, document.getElementById('app-node')
)

connect method

Earlier we used Providercomponents to inject the redux store into the application, the next thing we need to do is to connect the components and the store. And we know that Redux does not provide a way to directly manipulate the store state, we can only getStateaccess data through it, or dispatchchange the store state through an action.

This is exactly the ability provided by the connect high-order method provided by react-redux.

Example

container/TodoList.js

First, we create a list container component, which is responsible for obtaining the todo list in the component, and then passes the todos to the TodoList display component, and also passes the event callback function. When the display component triggers events such as clicks, the corresponding callback is called. The redux store state is updated through dispatch actions, and the react-redux connectmethod is used to finally connect the store and the presentation component , which receives

import {connect} from 'react-redux'
import TodoList from 'components/TodoList.jsx'

class TodoListContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {todos: null, filter: null}
  }
  handleUpdateClick (todo) {
    this.props.update(todo);  
  }
  componentDidMount() {
    const { todos, filter, actions } = this.props
    if (todos.length === 0) {
      this.props.fetchTodoList(filter);
    }
  render () {
    const { todos, filter } = this.props

    return (
      <TodoList 
        todos={todos}
        filter={filter}
        handleUpdateClick={this.handleUpdateClick}
        /* others */
      />
    )
  }
}

const mapStateToProps = state => {
  return {
    todos : state.todos,
    filter: state.filter
  }
}

const mapDispatchToProps = dispatch => {
  return {
    update : (todo) => dispatch({
      type : 'UPDATE_TODO',
      payload: todo
    }),
    fetchTodoList: (filters) => dispatch({
      type : 'FETCH_TODOS',
      payload: filters
    })
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoListContainer)

components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, handleUpdateClick }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} handleUpdateClick={handleUpdateClick} />
    ))}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.array.isRequired
  ).isRequired,
  handleUpdateClick: PropTypes.func.isRequired
}

export default TodoList

components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'

class Todo extends React.Component { 
  constructor(...args) {
    super(..args);
    this.state = {
      editable: false,
      todo: this.props.todo
    }
  }
  handleClick (e) {
    this.setState({
      editable: !this.state.editable
    })
  }
  update () {
    this.props.handleUpdateClick({
      ...this.state.todo
      text: this.refs.content.innerText
    })
  }
  render () {
    return (
      <li
        onClick={this.handleClick}
        style={{
          contentEditable: editable ? 'true' : 'false'
        }}
      >
        <p ref="content">{text}</p>
        <button onClick={this.update}>Save</button>
      </li>
    )
  }

Todo.propTypes = {
  handleUpdateClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

Container components and presentational components

When using Redux as a state management container for React applications, the practice of dividing components into Container Components and Presentational Components is usually implemented.

Presentational Components Container Components
Target UI display (HTML structure and style) Business logic (get data, update status)
Aware of Redux without have
Data Sources props Subscribe to the Redux store
change data Call the callback function passed by props Dispatch Redux actions
reusable Strong independence High degree of business coupling

Most of the code in the application is writing presentational components, and then using some container components to connect these presentational components to the Redux store.

connect() source code analysis

react-redux source logic

connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories;
selectorFactory = defaultSelectorFactory;
function connect (
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
  pure = true,
  areStatesEqual = strictEqual, // 严格比较是否相等
  areOwnPropsEqual = shallowEqual, // 浅比较
  areStatePropsEqual = shallowEqual,
  areMergedPropsEqual = shallowEqual,
  renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
  // props/context 获取store的键
  storeKey = 'store',
  ...extraOptions
  } = {}
) {
  const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
  const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
  
  // 调用connectHOC方法
  connectHOC(selectorFactory, {
    // 如果mapStateToProps为false,则不监听store state
    shouldHandleStateChanges: Boolean(mapStateToProps),
    // 传递给selectorFactory
    initMapStateToProps,
    initMapDispatchToProps,
    initMergeProps,
    pure,
    areStatesEqual,
    areOwnPropsEqual,
    areStatePropsEqual,
    areMergedPropsEqual,
    renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
    // props/context 获取store的键
    storeKey = 'store',
    ...extraOptions // 其他配置项
  });
}

strictEquall

function strictEqual(a, b) { return a === b }

shallowEquall

source code

const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  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 (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return true
}
shallowEqual({x:{}},{x:{}}) // false
shallowEqual({x:1},{x:1}) // true

connectAdvanced higher-order function

source code

function connectAdvanced (
  selectorFactory,
  {
    renderCountProp = undefined, // 传递给内部组件的props键,表示render方法调用次数
    // props/context 获取store的键
    storeKey = 'store',
    ...connectOptions
  } = {}
) {
  // 获取发布订阅器的键
  const subscriptionKey = storeKey + 'Subscription';
  const contextTypes = {
    [storeKey]: storeShape,
    [subscriptionKey]: subscriptionShape,
  };
  const childContextTypes = {
    [subscriptionKey]: subscriptionShape,
  };
  
  return function wrapWithConnect (WrappedComponent) {
    const selectorFactoryOptions = {
      // 如果mapStateToProps为false,则不监听store state
      shouldHandleStateChanges: Boolean(mapStateToProps),
      // 传递给selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      ...connectOptions,
      ...others
      renderCountProp, // render调用次数
      shouldHandleStateChanges, // 是否监听store state变更
      storeKey,
      WrappedComponent
    }
    
    // 返回拓展过props属性的Connect组件
    return hoistStatics(Connect, WrappedComponent)
  }
}

selectorFactory

selectorFactoryThe function returns a selector function, which calculates new props according to the store state, display component props, and dispatch, and finally injects the container component. The selectorFactoryfunction structure is as follows:

(dispatch, options) => (state, props) => ({
  thing: state.things[props.thingId],
  saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})

Note: The state in redux usually refers to the state of the redux store rather than the state of the component, and the props here are the props of the incoming component wrapperComponent.

source code

function defaultSelectorFactory (dispatch, {
  initMapStateToProps,
  initMapDispatchToProps,
  initMergeProps,
  ...options
}) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)
  
  // pure为true表示selectorFactory返回的selector将缓存结果;
  // 否则其总是返回一个新对象
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 最终执行selector工厂函数返回一个selector
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  );
}

pureFinalPropsSelectorFactory

function pureFinalPropsSelectorFactory (
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
  
  // 返回合并后的props或state
  // handleSubsequentCalls变更后合并;handleFirstCall初次调用
  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
    : handleFirstCall(nextState, nextOwnProps)
  }  
}

handleFirstCall

function handleFirstCall(firstState, firstOwnProps) {
  state = firstState
  ownProps = firstOwnProps
  stateProps = mapStateToProps(state, ownProps) // store state映射到组件的props
  dispatchProps = mapDispatchToProps(dispatch, ownProps)
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps) // 合并后的props
  hasRunAtLeastOnce = true
  return mergedProps
}

defaultMergeProps

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  // 默认合并props函数
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

handleSubsequentCalls

function handleSubsequentCalls(nextState, nextOwnProps) {
  // shallowEqual浅比较
  const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
  // 深比较
  const stateChanged = !areStatesEqual(nextState, state)
  state = nextState
  ownProps = nextOwnProps

  // 处理props或state变更后的合并
  // store state及组件props变更
  if (propsChanged && stateChanged) return handleNewPropsAndNewState()
  if (propsChanged) return handleNewProps()
  if (stateChanged) return handleNewState()
  
  return mergedProps
}

Compute returns new props

As long as the display component's own props change, you need to go back to the newly merged props, and then update the container component, regardless of whether the store state changes:

// 只有展示型组件props变更
function handleNewProps() {
  // mapStateToProps计算是否依赖于展示型组件props
  if (mapStateToProps.dependsOnOwnProps)
    stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps计算是否依赖于展示型组件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
  
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  
  return mergedProps
}
// 展示型组件props和store state均变更
function handleNewPropsAndNewState() {
  stateProps = mapStateToProps(state, ownProps)
  // mapDispatchToProps计算是否依赖于展示型组件props
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
  
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  
  return mergedProps
}

Compute returns stateProps

Usually container component props changes are driven by store state changes, so only store state changes are common, and this is where you need to pay attention when using Immutable: do not mapStateToPropsuse methods within toJS()methods.

When mapStateToPropsthe props object returned twice has not changed, there is no need to recalculate, just return the props object obtained by the merge before, and then compare the return value of the selector function twice in the selector tracking object to see if there is any change, it will return false, Container components do not trigger changes.

Because shallow comparison is used when comparing the results returned by mapStateToProps multiple times, the Immutable.toJS() method is not recommended. It returns a new object each time, and the comparison will return false. If Immutable is used and its content has not changed, then will return true, which can reduce unnecessary re-rendering.

// 只有store state变更
function handleNewState() {
  const nextStateProps = mapStateToProps(state, ownProps)
  // 浅比较
  const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
  stateProps = nextStateProps

  // 计算得到的新props变更了,才需要重新计算返回新的合并props
  if (statePropsChanged) {
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  }

  // 若新stateProps未发生变更,则直接返回上一次计算得出的合并props;
  // 之后selector追踪对象比较两次返回值是否有变更时将返回false;
  // 否则返回使用mergeProps()方法新合并得到的props对象,变更比较将返回true
  return mergedProps
}

hoist-non-react-statics

Similar to Object.assign, the non-React static properties or methods of the child component are copied to the parent component, and the React-related properties or methods will not be overridden but merged.

hoistStatics(Connect, WrappedComponent)

Connect Component

The real Connect high-order component connects the redux store state and incoming components, that is, maps the store state to the component props, react-redux uses the Provider component to inject the store through the context, and then the Connect component receives the store through the context and adds a subscription to the store :

class Connect extends Component {
  constructor(props, context) {
    super(props, context)

    this.state = {}
    this.renderCount = 0 // render调用次数初始为0
    // 获取store,props或context方式
    this.store = props[storeKey] || context[storeKey]
    // 是否使用props方式传递store
    this.propsMode = Boolean(props[storeKey])

    // 初始化selector
    this.initSelector()
    // 初始化store订阅
    this.initSubscription()
  }
  
  componentDidMount() {
    // 不需要监听state变更
    if (!shouldHandleStateChanges) return
    // 发布订阅器执行订阅
    this.subscription.trySubscribe()
    // 执行selector
    this.selector.run(this.props)
    // 若还需要更新,则强制更新
    if (this.selector.shouldComponentUpdate) this.forceUpdate()
  }
  
  // 渲染组件元素
  render() {
    const selector = this.selector
    selector.shouldComponentUpdate = false; // 重置是否需要更新为默认的false

    // 将redux store state转化映射得到的props合并入传入的组件
    return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

addExtraProps()

Add additional props properties to props:

// 添加额外的props
addExtraProps(props) {
  const withExtras = { ...props }
  if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 调用次数
  if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription

  return withExtras
}

Initialize selector tracking object initSelector

Selector, the selector, according to the redux store state and the component's own props, calculates the new props that will be injected into the component, caches the new props, and then compares the props obtained when the selector is executed again to determine whether the component needs to be updated. If the props change, the component is updated, otherwise it is not updated.

Use the initSelectormethod to initialize the selector tracking object and related state and data:

// 初始化selector
initSelector() {
  // 使用selector工厂函数创建一个selector
  const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
  // 连接组件的selector和redux store state
  this.selector = makeSelectorStateful(sourceSelector, this.store)
  // 执行组件的selector函数
  this.selector.run(this.props)
}

makeSelectorStateful()

Create a selector tracking object to track the results returned by the selector function:

function makeSelectorStateful(sourceSelector, store) {
  // 返回selector追踪对象,追踪传入的selector(sourceSelector)返回的结果
  const selector = {
    // 执行组件的selector函数
    run: function runComponentSelector(props) {
      // 根据store state和组件props执行传入的selector函数,计算得到nextProps
      const nextProps = sourceSelector(store.getState(), props)
      // 比较nextProps和缓存的props;
      // false,则更新所缓存的props并标记selector需要更新
      if (nextProps !== selector.props || selector.error) {
        selector.shouldComponentUpdate = true // 标记需要更新
        selector.props = nextProps // 缓存props
        selector.error = null
      }  
    }
  }

  // 返回selector追踪对象
  return selector
}

Initialize subscription initSubscription

Initialize listening/subscribing to redux store state:

// 初始化订阅
initSubscription() {
  if (!shouldHandleStateChanges) return; // 不需要监听store state

  // 判断订阅内容传递方式:props或context,两者不能混杂
  const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
  // 订阅对象实例化,并传入事件回调函数
  this.subscription = new Subscription(this.store, 
                                       parentSub,
                                       this.onStateChange.bind(this))
  // 缓存订阅器发布方法执行的作用域
  this.notifyNestedSubs = this.subscription.notifyNestedSubs
    .bind(this.subscription)
}

Subscription class implementation

The subscription publisher implementation used by the component subscription store:

export default class Subscription {
  constructor(store, parentSub, onStateChange) {
    // redux store
    this.store = store
    // 订阅内容
    this.parentSub = parentSub
    // 订阅内容变更后的回调函数
    this.onStateChange = onStateChange
    this.unsubscribe = null
    // 订阅记录数组
    this.listeners = nullListeners
  }
  
  // 订阅
  trySubscribe() {
    if (!this.unsubscribe) {
      // 若传递了发布订阅器则使用该订阅器订阅方法进行订阅
      // 否则使用store的订阅方法
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.onStateChange)
        : this.store.subscribe(this.onStateChange)
 
      // 创建订阅集合对象
      // { notify: function, subscribe: function }
      // 内部包装了一个发布订阅器;
      // 分别对应发布(执行所有回调),订阅(在订阅集合中添加回调)
      this.listeners = createListenerCollection()
    }
  }
  
  // 发布
  notifyNestedSubs() {
    this.listeners.notify()
  }
}

Subscription callback function

Callback function executed after subscription:

onStateChange() {
  // 选择器执行
  this.selector.run(this.props)

  if (!this.selector.shouldComponentUpdate) {
    // 不需要更新则直接发布
    this.notifyNestedSubs()
  } else {
    // 需要更新则设置组件componentDidUpdate生命周期方法
    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
    // 同时调用setState触发组件更新
    this.setState(dummyState) // dummyState = {}
  }
}

// 在组件componentDidUpdate生命周期方法内发布变更
notifyNestedSubsOnComponentDidUpdate() {
  // 清除组件componentDidUpdate生命周期方法
  this.componentDidUpdate = undefined
  // 发布
  this.notifyNestedSubs()
}

Other lifecycle methods

getChildContext () {
  // 若存在props传递了store,则需要对其他从context接收store并订阅的后代组件隐藏其对于store的订阅;
  // 否则将父级的订阅器映射传入,给予Connect组件控制发布变化的顺序流
  const subscription = this.propsMode ? null : this.subscription
  return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 接收到新props
componentWillReceiveProps(nextProps) {
  this.selector.run(nextProps)
}

// 是否需要更新组件
shouldComponentUpdate() {
  return this.selector.shouldComponentUpdate
}

componentWillUnmount() {
  // 重置selector
}

reference reading

  1. React with redux
  2. Smart and Dumb Components
  3. React Redux Container Pattern-

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325473119&siteId=291194637