Анализ исходного кода React-Redux

внешний интерфейс исходный код React.js Redux

Redux, как наиболее часто используемый инструмент для управления состоянием крупномасштабных приложений React, его концепции, теория и практика заслуживают изучения, анализа и затем глубокого понимания на практике, что очень полезно для развития фронтенд-разработчиков. . В этой статье планируется объединить различия между компонентами контейнера Redux и компонентами отображения и наиболее распространенной библиотекой соединений между приложениями Redux и React, анализом исходного кода react-redux, чтобы достичь более глубокого понимания приложений Redux и React.

Добро пожаловать в мой личный блог

предисловие

Библиотека React-redux обеспечиваетProviderКомпонент внедряет хранилище в приложение через контекст, а затем может использоватьconnectВысокоуровневый метод получает и отслеживает хранилище, затем вычисляет новые реквизиты на основе состояния хранилища и собственных реквизитов компонента, внедряет их в компонент и сравнивает рассчитанные новые реквизиты, наблюдая за хранилищем, чтобы определить, нужно ли компоненту быть обновленным. обновлено.

react与redux应用结构
Структура приложения React и Redux

Provider

Во-первых, библиотека react-redux предоставляетProviderКомпоненты внедряют хранилище в компонент входа всего приложения React, обычно это компонент верхнего уровня приложения.ProviderКомпонент использует контекст для передачи хранилища:

// 内部组件获取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')
)

способ подключения

Ранее мы использовалиProviderКомпонент внедряет хранилище избыточности в приложение, и следующее, что вам нужно сделать, это соединить компонент и хранилище. И мы знаем, что Redux не позволяет напрямую манипулировать состоянием хранилища, мы можем использовать только егоgetStateполучить доступ к данным или черезdispatchДействие по изменению состояния магазина.

Это именно та возможность, которую предоставляет метод высокого порядка connect, предоставляемый react-redux.

Example

container/TodoList.js

Сначала мы создаем компонент-контейнер списка, который отвечает за получение списка задач в компоненте, а затем передает задачи компоненту презентации TodoList, а также передает функцию обратного вызова события. щелчок, вызывается соответствующий обратный вызов.Обновите состояние хранилища избыточности с помощью действий отправки и, наконец, подключите компоненты хранилища и презентации, используя react-reduxconnectметод, который получает

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

Контейнерные компоненты и презентационные компоненты

При использовании Redux в качестве контейнера управления состоянием для приложений React обычно реализуется практика разделения компонентов на Container Components и Presentational Components.

Presentational Components Container Components
Цель Представление пользовательского интерфейса (структура и стиль HTML) Бизнес-логика (получение данных, обновление статуса)
Осведомлен о Редукс без имеют
Источники данных props Подпишитесь на магазин Redux
изменить данные Вызовите функцию обратного вызова, переданную реквизитами Dispatch Redux actions
многоразовый Сильная независимость Высокая степень связанности бизнеса

Большая часть кода в приложении — это написание презентационных компонентов, а затем использование некоторых компонентов-контейнеров для подключения этих презентационных компонентов к хранилищу Redux.

анализ исходного кода connect()

react-redux源码逻辑
исходная логика реакции-редукции

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

исходный код

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

connectДополнительная функция высшего порядка

исходный код

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

selectorFactoryФункция возвращает функцию-селектор, которая вычисляет новые реквизиты в соответствии с состоянием хранилища, реквизитами отображаемого компонента и отправкой, и, наконец, внедряет компонент-контейнер.selectorFactoryСтруктура функции такая:

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

Примечание. Состояние в избыточности обычно относится к состоянию хранилища избыточности, а не к состоянию компонента, а реквизиты здесь — это реквизиты входящего компонента wrapperComponent.

исходный код

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 возвращает новые реквизиты

Пока собственные реквизиты компонента отображения изменяются, вам нужно вернуться к недавно объединенным реквизитам, а затем обновить компонент контейнера, независимо от того, изменилось ли состояние хранилища:

// 只有展示型组件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 возвращает stateProps

Обычно изменения свойств компонента контейнера управляются изменениями состояния хранилища, поэтому во многих случаях изменяется только состояние хранилища, и именно на это нужно обратить внимание при использовании Immutable:УходитеmapStateToPropsиспользовать в методеtoJS()метод.

когдаmapStateToPropsКогда объект реквизита, возвращаемый дважды, не изменился, нет необходимости его пересчитывать, просто верните объект реквизита, полученный в результате предыдущего слияния напрямую, а затем дважды сравните возвращаемое значение функции селектора в объекте отслеживания селектора, чтобы увидеть, есть ли любое изменение, он вернет false, компоненты контейнера не вызывают изменений.

Поскольку поверхностное сравнение используется при многократном сравнении результатов, возвращаемых mapStateToProps, метод Immutable.toJS() не рекомендуется. Он каждый раз возвращает новый объект, и сравнение возвращает false. Если используется Immutable, а его содержимое не изменено, то вернет true, что может уменьшить ненужный повторный рендеринг.

// 只有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

Как и в случае с Object.assign, не связанные с React статические свойства или методы дочернего компонента копируются в родительский компонент, а связанные с React свойства или методы не переопределяются, а объединяются.

hoistStatics(Connect, WrappedComponent)

Connect Component

Настоящий высокоуровневый компонент Connect соединяет состояние хранилища redux и входящие компоненты, то есть сопоставляет состояние хранилища с реквизитами компонента, react-redux использует компонент Provider для внедрения хранилища через контекст, а затем компонент Connect получает store через контекст и добавляет подписку в 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()

Добавьте дополнительные свойства props в 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
}

Инициализировать объект отслеживания селектора initSelector

Селектор, селектор, в соответствии с состоянием хранилища избыточности и собственными реквизитами компонента, вычисляет новые реквизиты, которые будут введены в компонент, и кэширует новые реквизиты, а затем снова выполняет селектор, сравнивая полученные реквизиты, чтобы решить, следует ли обновить компонент.Если реквизиты меняются, компонент обновляется, иначе не обновляется.

использоватьinitSelectorМетод инициализирует объект отслеживания селектора и связанное с ним состояние и данные:

// 初始化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()

Создайте объект отслеживания селектора, чтобы отслеживать результаты, возвращаемые функцией селектора:

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
}

Инициализировать подписку initSubscription

Инициализировать прослушивание/подписку на состояние хранилища избыточности:

// 初始化订阅
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)
}

Реализация класса подписки

Реализация издателя подписки, используемая хранилищем подписок компонентов:

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()
  }
}

Функция обратного вызова подписки

Функция обратного вызова, выполняемая после подписки:

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()
}

Другие методы жизненного цикла

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
}

эталонное чтение

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