之前写过一篇 Redux 的源码解析文章,时隔几个月我又看了看 React Redux 的源码,这一次也是收获满满,所以写下了这篇博客记录一下我的收获。

React Redux 不同于 Redux,Redux 的设计目的在于提供一个独立于 UI 的数据中心,使得我们可以方便地在组件树中的任意多个组件间共享数据;Redux 独立于 React,可以脱离 React 使用。而 React Redux 是为了方便我们将 Redux 与 React 结合使用,使得我们可以在 React 组件内方便地获取 Store 中的数据并且订阅 Store 内数据的变化;当 Store 内数据变化后,能够使得我们相应的组件根据一定的条件重新渲染。

所以 React Redux 的核心点在于:

  • 提供数据给我们的组件。
  • 订阅 Store 的更新,及时 re-render 相关组件。
  • 提供 api 给我们的组件,是得我们可以在组件内可以发起对 Store 数据的更改。

这篇文章可以就这两点围绕展开解读。

在阅读源码之前,最好熟知如何使用 React-Redux,如果对于 API 还不熟悉的话,可以阅读官网的相关文档

另外在你的电脑上最好打开一份同版本的源代码项目,以便跟随阅读。

首先我们下载 GitHub 上的 React Redux 源码,我所阅读的源码版本是 7.1.3,读者也最好是下载该版本的源码,以免在阅读时产生困惑。

git clone https://github.com/reduxjs/react-redux.git

下载下来之后我们就可以看到源码的文件结构了,src 目录具体文件结构如下:

src
├── alternate-renderers.js
├── components
│   ├── Context.js
│   ├── Provider.js
│   └── connectAdvanced.js
├── connect
│   ├── connect.js
│   ├── mergeProps.js
│   ├── mapDispatchToProps.js
│   ├── mapStateToProps.js
│   ├── selectorFactory.js
│   ├── verifySubselectors.js
│   └── wrapMapToProps.js
├── hooks
│   ├── useDispatch.js
│   ├── useReduxContext.js
│   ├── useSelector.js
│   └── useStore.js
├── index.js
└── utils
    ├── Subscription.js
    ├── batch.js
    ├── isPlainObject.js
    ├── reactBatchedUpdates.js
    ├── reactBatchedUpdates.native.js
    ├── shallowEqual.js
    ├── useIsomorphicLayoutEffect.js
    ├── useIsomorphicLayoutEffect.native.js
    ├── verifyPlainObject.js
    ├── warning.js
    └── wrapActionCreators.js

我们先从入口文件 index.js 开始看起。

该文件内容比较简单:

import Provider from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import { ReactReduxContext } from './components/Context'
import connect from './connect/connect'

import { useDispatch, createDispatchHook } from './hooks/useDispatch'
import { useSelector, createSelectorHook } from './hooks/useSelector'
import { useStore, createStoreHook } from './hooks/useStore'

import { setBatch } from './utils/batch'
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
import shallowEqual from './utils/shallowEqual'

setBatch(batch)

export {
  Provider,
  connectAdvanced,
  ReactReduxContext,
  connect,
  batch,
  useDispatch,
  createDispatchHook,
  useSelector,
  createSelectorHook,
  useStore,
  createStoreHook,
  shallowEqual
}

主要就是从各个文件中导出我们需要使用的 API。

我们使用的最多的几个 API 是 Providerconnect,先从这两个看起,其他 API 放到后面看。

一、Provider

根据 React-Redux 官网的实例,我们在使用的时候需要引入 Provider 组件,然后将其包裹在我们的根组件外边,传入 store 数据:

React-Redux 源码解析

它使得我们的应用可以方便地获取 store 对象,那么它是如何实现的呢?

我们知道 React 有一个概念叫 Context,它同样提供一个 Provider 组件,并且可以使得 Consumer 可以在内部的任意位置获取 Provider 提供的数据,所以这两者有非常相似的功能和特性。React-Redux 就是基于 Context API 实现的,这样顶层组件提供的 store 对象可以在内部位置获取到。

React-Redux 的 Provider 组件源码如下:

function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])

  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

可以看到这是一个简单地函数组件,在开始的时候创建了一个 contextValue 对象,内部包含一个 store 和一个 subscription。这个 contextValue 会在最后作为 value 传入 Context API,即最后一行代码。

这里值得学习的是 contextValue 的计算方法,基于 useMemo Hook 实现了对于 contextValue 的缓存,只有当 store 发生变化的时候这个值才会重新计算,减少了计算的开支。

这个 store 对象是我们内部组件所需要的,那这个 subscription 对象是啥呢?这个 subscription 对象其实是 React-Redux 实现数据订阅的关键所在,我们之后再关注这一块,现在只需要知道这是很重要的一个内容就行。

关于 Context 的来源,可以看到是 Provider 接受到的 context props 或者内部默认的 ReactReduxContext。所以我们知道了我们可以提供一个默认的 context 组件给 Provider 来实现进一步的封装。这里多数情况下我们都是使用的 ReactReduxContext:

export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)

if (process.env.NODE_ENV !== 'production') {
  // 在非生产环境中可以在 devtools 中看到该组件名称
  ReactReduxContext.displayName = 'ReactRedux'
}

可以看到这是通过 React.createContext API 创建的一个 context 对象,平平无奇。

那么我们通过 Context.Provider 提供了我们的 contextValue 给下层组件,那么我们的下层组件是如何获取我们的 contextValue 的呢?

这个时候我们就应该想到我们的 connect 函数,肯定是它内部完成了这些工作,下面我们看看 connect 函数做了什么。

二、connect

connect 函数来源于 connect.js 的 createConnect 函数调用,这是一个高阶函数,返回了我们真正使用到的 connect API:

// createConnect with default args builds the 'official' connect behavior. Calling it with
// different options opens up some testing and extensibility scenarios
export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  // 这是我们真正使用到的 connect 函数
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
    // match 函数的第二个参数是一个函数数组,通过将第一个参数作为调用参数,依次顺序调用第二个参数内的函数来获取结果
    // 返回的函数就是经过包装后的对应函数
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect',

      // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })
  }
}

为什么会有这么个高阶函数来产生我们所使用的 connect 函数呢?第一段注释说得很清楚了,因为给 createConnect 函数传入不同的参数可以生成不同的 connect 函数,用于我们的测试或者其他场景,在计算我们真正使用的 connect 函数时,使用到的全部都是默认参数。

createConnect 函数返回了我们真正使用到的 connect 函数,这个函数所接受的参数我们就应该比较熟悉了。如果还不熟悉的话,可以参考 React-Redux 官方文档

connect 函数在接受到我们传入的参数后,会执行三次 match 函数来计算初始化 mapDispatchToPropsmapStateToPropsmergeProps 的函数。我们看看 match 函数是如何定义的:

/*
  connect is a facade over connectAdvanced. It turns its args into a compatible
  selectorFactory, which has the signature:

    (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps

  connect passes its args to connectAdvanced as options, which will in turn pass them to
  selectorFactory each time a Connect component instance is instantiated or hot reloaded.

  selectorFactory returns a final props selector from its mapStateToProps,
  mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps,
  mergePropsFactories, and pure args.

  The resulting final props selector is called by the Connect component instance whenever
  it receives new props or store state.
 */

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
      }.`
    )
  }
}

match 函数的第一个参数是我们传入的原始的 mapStateToPropsmapDispatchToProps 或者 mergeProps 函数。第二个参数是 connect 函数传入的一个数组,第三个参数是指定的函数名称。

在 match 函数内,会遍历第二个数组参数,依次执行数组中的函数,并且将我们原始的 mapxxxToProps 或者 mergeProps 函数作为参数传入这个参数,如果返回的结果为非 False 值,就直接返回,作为我们的 init..函数。

如果数组遍历完成后还是没有得到非 False 的返回值,那么就返回一个__标准格式__ (注意返回函数的格式)的报错函数,说明用户传入的 map.. 函数不符合要求,看起来 match 函数会对我们传入的参数做一次校验。

那么上一段中提到的标准格式是什么格式呢?我们可以看一下函数定义上面的大段注释。这段注释说明 connect 函数只是 connectAdvanced 函数的一个代理人,它所做的工作就是将我们传入的参数转化为可以供 selectorFactory 使用的参数。而可以供 selectorFactory 使用的一个标准就是参数的结构符合如下定义:

(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps

这个结构就是我们的标准格式。

被转化为这个格式的 init 系列函数会被作为参数传入 connectAdvanced 函数,而 connectAdvanced 函数会根据传入的参数和 store 对象计算出最终组件需要的 props。

这个标准格式非常重要,因为后面的很多地方的代码都跟这个有关,所以我们需要注意一下。

我们已经知道了 match 函数的作用,所以我们接下来看一下 match 是如何通过第二个参数来计算我们的标准化后的 mapStateToProps……等函数的。

2.1 initMapStateToProps

这个函数是根据下面代码计算出来的:

const initMapStateToProps = match(
  mapStateToProps,
  mapStateToPropsFactories,
  'mapStateToProps'
)

match 的原理我们已经明白了,所以标准化的关键就在于 mapStateToPropsFactories 函数数组。

我们现在看一下这个数组:

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]

可以看到这个函数数组一共就有两个函数,会一次传入我们的 mapStateToProps 参数进行计算,得到了结果就会返回。

第一个函数 whenMapStateToPropsIsFunction 是计算当我们的 mapStateToProps 参数为函数时的结果,第二个函数时计算当我们传入的 mapStateToProps 是一个 False 值时的默认结果(即如果我们的 mapStateToProps 为 null 时,selectorFactory 函数应该使用的函数)。

我们再深入看看 wrapMapToPropsFuncwrapMapToPropsConstant 函数,其中wrapMapToPropsFunc 函数是重点。

wrapMapToPropsFunc 函数代码如下:

// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction,
// this function wraps mapToProps in a proxy function which does several things:
//
//  * Detects whether the mapToProps function being called depends on props, which
//    is used by selectorFactory to decide if it should reinvoke on props changes.
//
//  * On first call, handles mapToProps if returns another function, and treats that
//    new function as the true mapToProps for subsequent calls.
//
//  * On first call, verifies the first result is a plain object, in order to warn
//    the developer that their mapToProps function is not returning a valid result.
//
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 ownProps
    // 第一次运行时将 dependsOnOwnProps 设置成 true
    // 使得 detectFactoryAndVerify 在运行的时候可以获得第二个参数,等第二次运行时
    // proxy.mapToProps 和 denpendsOnOwnProps 都是经过计算得到的
    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(
      stateOrDispatch,
      ownProps
    ) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)

      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      if (process.env.NODE_ENV !== 'production')
        verifyPlainObject(props, displayName, methodName)

      return props
    }

    return proxy
  }
}

可以看到该函数返回的结果确实符合我们的标准格式,并且 proxy.mapToProps 返回的结果即为标准格式中的最终结果:nextFinalProps

可以看到返回的 initProxySelector 函数可以接受 dispatch 函数和 options 参数,返回一个 proxy 函数对象,这个函数接受 stateOrDispatch 函数和 ownProps 参数,最终返回一个计算结果。

这里的关键在于 proxy.dependsOnOwnProps 属性,这个属性决定了我们在调用 proxy.mapToProps 函数时是否传入第二个函数。那这个 dependsOnOwnProps 属性是如何得到的呢?

继续看代码,可以发现在第一次执行 initProxySelector 函数的时候,默认 dependsOnOwnProps 参数为 true,这是为了让 detectFactoryAndVerify 函数执行时可以得到 ownProps 这个参数。而detectFactoryAndVerify 的存在是为了在第一次运行 mapToProps 函数时进行一些额外的工作,比如计算dependsOnOwnProps 属性、校验返回的 props 结果。在 detectFactoryAndVerify 函数内部会重新为 proxy.mapToProps 赋值,这意味着第二次运行 proxy.mapToProps 函数的时候,就不会重新计算那些参数了。

另外如果我们的 mapStateToProps 返回的结果是一个函数,则在后续的计算中,这个返回的函数会作为真正的 mapToProps 函数进行 props 的计算。这也是为什么官方文档中会有如下这段话:

You may define mapStateToProps and mapDispatchToProps as a factory function, i.e., you return a function instead of an object. In this case your returned function will be treated as the real mapStateToProps or mapDispatchToProps, and be called in subsequent calls. You may see notes on Factory Functions or our guide on performance optimizations.

也就是除了可以返回一个纯对象以外,还可以返回一个函数。

再来看一下 getDependsOnOwnProps 函数是如何计算 dependsOnOwnProps 属性的:

// 该函数用于计算 mapToProps 是否需要使用到 props
// 依据是 function.length,如果 function.length === 1 就说明只需要 stateOrDispatch
// 如果 function.length !== 1,说明就需要 props 进行计算。
export function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null &&
    mapToProps.dependsOnOwnProps !== undefined
    ? Boolean(mapToProps.dependsOnOwnProps)
    : mapToProps.length !== 1
}

mapToProps.dependsOnOwnProps 有值时就直接使用这个值作为结果,不再重新计算;如果还是没有值的话, 需要进行一次计算,计算的逻辑就是 mapToProps 函数的参数格式,即我们传递给 connect 函数的 mapStateToProps 函数的参数个数,只有当参数个数为 1 的时候才不会传入 ownProps 参数。

关于 wrapMapToPropsConstant 函数,这是用来计算当我们传入的 mapStateToProps 函数为 null 时的结果的函数。代码如下:

export function wrapMapToPropsConstant(getConstant) {
  return function initConstantSelector(dispatch, options) {
    const constant = getConstant(dispatch, options)

    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}

可以看到也是对结果进行了一下标准化,然后计算得到的常量 constant,返回 constantSelector 作为结果,其 dependsOnOwnProps 属性为 fasle,因为我们没有传入对应参数,也就没有依赖 ownProps 了。最终得到的结果就是一个 undefined 对象,因为这种情况下,getConstant 函数为一个空函数: () => {}

2.2 initMapDispatchToProps

计算该函数用到的函数数组为:

import { bindActionCreators } from 'redux'
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
]

这里会有三个函数,分别用于处理传入的 mapDispatchToProps 是函数、null 和对象时的情况。

当传入的 mapDispatchToProps 为函数时,同样也是调用 wrapMapToPropsFunc 计算结果,这个与 initMapStateToProps 的计算逻辑一致。

当传入的 mapDispatchToProps 为 null 时,处理的逻辑同 initMapStateToProps,区别在于传入的参数不是空函数,而是一个返回对象的函数,对象默认包含 dispatch 函数,这就使得我们使用 React-Redux 以后,可以在内部通过 this.props.dispatch 访问到 store 的 dispatch API:

Once you have connected your component in this way, your component receives props.dispatch. You may use it to dispatch actions to the store.

当传入的 mapDispatchToProps 为对象时,说明这是一个 ActionCreator 对象,可以通过使用 redux 的 bindActionCreator API 将这个 ActionCreator 转化为包含很多函数的对象并 merge 到 props。

2.3 initMergeProps

这一个函数的计算比较简单,代码如下:

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) {
        // 这里如果 pure === true 并且新旧 props 内容未变的时候
        // 就不对 mergedProps 进行赋值,这样可以确保原来内容的引用不变,
        // 可以让我们的 useMemo 或者 React.memo 起作用。
        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]

基本上就是直接对 statePropsdispatchPropsownProps 三者的合并,加上了一些基本的校验。

现在我们得到了三个主要的函数:initMapStateToPropsinitMapDispatchToPropsinitMergeProps。我们知道了 React-Redux 是如何通过我们传入的参数结合 store 计算出被 connect 的组件的 props 的。

下面我们再来进一步了解一下,selectorFactory 函数是如何基于我们的 init... 系列函数计算最终的 props 的。

2.4 selectorFactory

找到文件中的 selectorFactory.js 文件,可以看到 finalPropsSelectorFactory 函数,这个就是我们的 selectorFactory 函数。

代码如下:

export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if (process.env.NODE_ENV !== 'production') {
    verifySubselectors(
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      options.displayName
    )
  }

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

可以看到一开始通过 init... 系列函数计算出了需要的 mapStateToPropsmapDispatchToPropsmergeProps 函数。

随后根据 options.pure 的值选择不同的函数进行下一步计算。

options.pure === true 时,意味着我们的组件为”纯组件“。

React Redux 源码不得不提的一个点就是配置项中的 pure 参数,我们可以在 createStore 的时候传入该配置,该配置默认为 true。

当 pure 为 true 的时候,React Redux 内部有几处地方就会针对性地进行优化,比如我们这里看到的 selectFactory。当 pure 为不同的值时选择不同的函数进行 props 的计算。如果我们的 pure 为 false,则每次都进行相应计算产生新的 props,传递给我们的内部组件,触发 rerender。

当我们的 pure 为 true 的时候,React Redux 会缓存上一次计算的相应结果,然后在下一次计算后对比结果是否相同,如果相同的话就会返回上一次的计算结果,一旦我们的组件是纯组件,则传入相同的 props 不会导致组件 rerender,达到了性能优化的目的。

当 pure 为 false 时,调用 impureFinalPropsSelectorFactory 计算 props:

export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}

这样每次计算都会返回新的 props,导致组件一直 rerender。

当 pure 为 true 时,调用 pureFinalPropsSelectorFactory 计算 props:

export function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let 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
  }

  function handleNewPropsAndNewState() {
    stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewProps() {
    if (mapStateToProps.dependsOnOwnProps)
      stateProps = mapStateToProps(state, ownProps)

    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewState() {
    const nextStateProps = mapStateToProps(state, ownProps)
    const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
    stateProps = nextStateProps

    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

    return mergedProps
  }

  function handleSubsequentCalls(nextState, nextOwnProps) {
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps

    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}

可以看到第一次计算时的过程跟 impureFinalPropsSelectorFactory 一致,但是多了个闭包内缓存的过程,在随后的 props 计算当中,会根据 state 和 props 的变化情况选择不同的函数进行计算,这样做是为了尽可能的减少计算量,优化性能。如果 state 和 props 都没有发生变化的话,就直接返回缓存的 props。

可以看到这段代码里面对比变量是否不同的函数有这么几个:areOwnPropsEqualareStatesEqualareStatePropsEqual。在前文中我们还看到过 areMergedPropsEqual 这个函数,他们都在 connect 函数定义时已经被赋值:

areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual

strictEqual 的定义:

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

shallowEqual 的定义如下:

// 眼尖的朋友可能会发现这段代码来自于 React 源码
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 (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false
    }
  }

  return true
}

可以看到前者的对比简单粗暴,后者的对比更加细腻。

为什么 state 的对比会跟其他三者不一样呢?

这是因为 state 比较特殊,查看 Redux 的源码:combineReducers.ts。不难发现当我们的所有 reducers 返回的内容不变(维持原有的引用)时,我们最终得到的 state(Store.getState() 返回的对象)也会维持原有引用,使得 oldState === newState 成立,所以我们在 React Redux 中对于 state 的对比会比其他三个要简单许多。

为什么 Redux 能够确保 reducer 没有修改 state 的时候返回的是原来的 state,而 reducer 修改后的 state 的引用关系一定发生了变化呢?是因为 redux 要求使用者在定义 reducer 时按照这样的要求做,在 reducer 产生新数据时一定要新建对象(通过扩展语法... 或者 Object.assisgn),在没有匹配到 action.type 时一定返回旧对象。这一点可以在之前提到的 combineReducers 的源码中仍然可以看到许多针对性的检查。

看完 2.4 小节,我们其实可以发现 selectorFactory 确实符合第二大节开始时提到的标准格式:

(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps

我希望大家能够记住这个结论,因为有助于我们后面理解相关的代码。

了解了 selectorFactory 的工作原理之后,我们再看看在 connectAdvanced 内部是如何使用它的。

2.5 connectAdvanced

我们已经知晓了是 React Redux 如何通过 state 和 props 计算出下一次渲染所需要使用的 props。这一节回到 connectAdvenced 函数看看我们的 props 是在什么时机被计算的。

connectAdvanced 函数实际上就是我们使用到的 connect 函数,它接受相应配置以及相关组件,返回给我们一个高阶组件。

打开 src/components/connectAdvanced.js 文件,可以看到该函数在前面一小部分做了部分校验之后,直接返回了一个拥有一大段代码的函数:wrapWithConnect,这个函数大概有三百多行,它就是我们执行 connect(...) 之后返回的函数,可想而知该函数接受一个__我们定义的 UI 组件,返回给我们一个容器组件__。

我们依次看下去这段代码,拣一部分最重要的代码进行分析。

 const selectorFactoryOptions = {
   ...connectOptions,
   getDisplayName,
   methodName,
   renderCountProp,
   shouldHandleStateChanges,
   storeKey,
   displayName,
   wrappedComponentName,
   WrappedComponent
 }

 const { pure } = connectOptions

 function createChildSelector(store) {
   return selectorFactory(store.dispatch, selectorFactoryOptions)
 }

首先可以看到这段代码,它根据相关参数构建了一个 selectorFactoryOptions 对象,然后新建了一个 createChildSelector 函数,这个函数用于调用我们之前分析过的 selectorFactory 函数,我们知道 selectorFactory 函数符合我们的标准格式,所以调用 selectorFactory 会得到一个新的函数,该函数接受 state 和 props,返回计算出的下一次渲染所需要的 props。所以 createChildSelector 得到的结果就是一个 props 计算函数。这里之所以要这么做是为了计算得到当前 store 需要用到的 props 计算函数,防止后面需要计算时又要重新调用。而当我们的 store 对象发生变化以后,这个函数又会被重新调用:

// childPropsSelector 函数用于根据 mapStateToProps、mapDispatchToProps 等配置
// 计算 store 更新后的组件 props
const childPropsSelector = useMemo(() => {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return createChildSelector(store)
}, [store])

可以看到这里也使用 useMemo 进行了优化。

昨晚一些前置工作之后,内部定义了一个 ConnectFunction,这就是我们真正用于渲染的组件,最后会被 connect()() 返回。我们向容器组件传递 props 的时候,就是传递给了 ConnectFunction。

2.6 ConnectFunction

在一开始,ConnectFunction 会准备一些将要用到的数据:

const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
  // Distinguish between actual "data" props that were passed to the wrapper component,
  // and values needed to control behavior (forwarded refs, alternate context instances).
  // To maintain the wrapperProps object reference, memoize this destructuring.
  const { forwardedRef, ...wrapperProps } = props
  return [props.context, forwardedRef, wrapperProps]
}, [props])

// 根据 propsContext 和 Context 计算要使用的 context,其中 propsContext 来自于外部传递,Context 来自于内部
// 如果 ContainerComponent 有接受到 context 属性,那么就是用传入的这个 context,否则使用 Context.js 定义的那个。
// 同时那也是提供 store 对象的那个 context。
const ContextToUse = useMemo(() => {
  // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
  // Memoize the check that determines which context instance we should use.
  return propsContext &&
    propsContext.Consumer &&
    isContextConsumer(<propsContext.Consumer />)
    ? propsContext
    : Context
}, [propsContext, Context])

// 通过 useContext 使用 context,并且订阅其变化,当 context 变化时会 re-render。
// Retrieve the store and ancestor subscription via context, if available
const contextValue = useContext(ContextToUse)

// The store _must_ exist as either a prop or in context.
// We'll check to see if it _looks_ like a Redux store first.
// This allows us to pass through a `store` prop that is just a plain value.
// 检查 props 以及 context 中是否有 store,如果都没有那就没法玩了。
// 所以这里我们其实也可以给 ContainerComponent 传入一个 store props 作为我们 connect 的 store
const didStoreComeFromProps =
      Boolean(props.store) &&
      Boolean(props.store.getState) &&
      Boolean(props.store.dispatch)
const didStoreComeFromContext =
      Boolean(contextValue) && Boolean(contextValue.store)
// Based on the previous check, one of these must be true
// 使用传入的 store,如果 ContainerComponent 接收到了 store,那就用它作为 store。
// 实验证明确实可以使用 DisplayComponent 的 props 传入 store,并且如果传入了的话,该组件就会使用 props 中的 store 而非 context 中的。
const store = didStoreComeFromProps ? props.store : contextValue.store

上面代码中的 props 就是我们传递给容器组件的 props,首先会从其中解析出我们的 forwardedRef、context 和 其他 props 属性。

forwardedRef 用于将我们在容器组件上设置的 ref 属性通过 React.forwardRef API 转交给内部的 UI 组件。

context 属性用于计算我们将要使用到的 context 。其他 props 用于计算 UI 组件需要用到的 props。

当决定了要使用哪个 context 的时候,就会通过 useContext API 使用其传递的值,所以我们这里用到的是 React Hooks,我们通过 useContext API 即可使用到 Provider 内部的内容,而无需使用 Consumer 组件。

上面的最后一段代码用于判断我们的 store 应该用哪个来源的数据,可以看到如果我们给我们的容器组件传递了 store 属性的话,React Redux 就会使用这个 store 作为数据来源,而不是顶层 Context 内的 store 对象。

如果我们先不考虑组件是如何订阅 store 更新的话,我们可以先看 UI 组件需要的 props 是如何计算出来并且应用的。

// childPropsSelector 函数用于根据 mapStateToProps、mapDispatchToProps 等配置
// 计算 store 更新后的组件 props
const childPropsSelector = useMemo(() => {
  // The child props selector needs the store reference as an input.
  // Re-create this selector whenever the store changes.
  return createChildSelector(store)
}, [store])

这段代码我们之前有分析过,createChildSelector 函数用于调用 selectorFactory,返回 selectorFactory 调用一次之后的结果,由于 selectorFactory 符合我们的设计规范:

(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps

所以 childPropsSelector 返回的函数就符合下面的规范:

(nextState, nextOwnProps) => nextFinalProps

在整个函数的后半段,会有下面这段计算代码:

// If we aren't running in "pure" mode, we don't want to memoize values.
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
// that just executes the given callback immediately.
const usePureOnlyMemo = pure ? useMemo : callback => callback()

// 最终使用的 props
const actualChildProps = usePureOnlyMemo(() => {
  // ...忽略部分代码
  return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])

可以看到这里也根据 options.pure 选项决定是否缓存计算结果,如果不是 true 的话会每次更新 store、previousStateUpdateResult 或者 wrapperProps 都会导致 actualChildProps 重新计算。

所以这里的 actualChildProps 就是我们上方规范中的 nextFinalProps。

计算出最终用到的 props 之后,就开始渲染我们的 UI 组件了:

// Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
  () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
  [forwardedRef, WrappedComponent, actualChildProps]
)

上方代码中的 WrappedComponent 组件即为我们传入的 UI 组件,可以看到最后 forwardedRef(本来是容器组件的 ref)最终被指到了内部的 UI 组件上。

renderedWrappedComponent 组件就是我们渲染 UI 组件的结果,然而我们还不能直接拿去返回给用户渲染,我们还要考虑其他情况,我们接下来看看 UI 组件是如何订阅 store 更新的。

三、Subscriptions

在我们的阅读源码的时候,可能经常会看到这个 subscription 对象,这个对象用于实现组件对于 store 更新的订阅,是 React Redux 实现数据更新的关键。接下来我们深入该 API 的实现及功能。

打开我们的 src/utils/Subscription.js 文件,该文件总共就两个函数:createListenerCollectionSubscription。前者是辅助工具,后者是我们的真正使用到的 API。

先看这个工具函数:

const CLEARED = null
const nullListeners = { notify() {} }

function createListenerCollection() {
  const batch = getBatch()
  // the current/next pattern is copied from redux's createStore code.
  // TODO: refactor+expose that code to be reusable here?
  // 此处使用两个队列,防止在 notify 的同时进行 subscribe 导致的边缘行为
  // Reference: https://github.com/reduxjs/react-redux/pull/1450#issuecomment-550382242
  let current = []
  let next = []

  return {
    clear() {
      next = CLEARED
      current = CLEARED
    },

    notify() {
      const listeners = (current = next)
      batch(() => {
        for (let i = 0; i < listeners.length; i++) {
          listeners[i]()
        }
      })
    },

    get() {
      return next
    },

    subscribe(listener) {
      let isSubscribed = true
      if (next === current) next = current.slice()
      next.push(listener)

      return function unsubscribe() {
        if (!isSubscribed || current === CLEARED) return
        isSubscribed = false

        if (next === current) next = current.slice()
        next.splice(next.indexOf(listener), 1)
      }
    }
  }
}

可以看到在这个函数返回的对象内部定义了两个队列,一个 next,一个 current,他们用于存放订阅当前对象更新的 listener,一个用于存放下一步更新后的队列。这样做是为了防止在 notify 执行后,队列被遍历时又开始调用 subscribe 或者 unsubscribe 函数导致队列发生变化导致的一些边缘问题。每次 notify 之前都会同步 current 为 next,随后的 subscribe 执行都是在 next 的基础上执行。

总而言之这是一个返回纯对象的函数,而这个对象的作用就是一个事件发布订阅中心,这是属于观察者模式的应用,我们的所有 listener 都会监听当前对象,一旦当前对象调用 notify,所有 listener 都会被执行。而这个对象的 subscribe 或者 notify 的调用时机取决于该对象的使用者。

下面是 subscription 对象的源码:

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    // 绑定好 this,因为之后会在其他地方执行
    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  addNestedSub(listener) {
    // 先执行 trySubscribe 函数,确定当前实例的订阅目标(parentSub or store)
    this.trySubscribe()
    // 子订阅都集中在 this.listeners 进行管理
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    // this.onStateChange 由外部提供
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)

      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

我们先看其构造函数,总共接受了两个参数:storeparentSub,这个 store 就是你所想到的 store,是 Redux 的数据中心。它在该类的另一个函数 trySubscribe 中被使用到:

trySubscribe() {
  if (!this.unsubscribe) {
    this.unsubscribe = this.parentSub
      ? this.parentSub.addNestedSub(this.handleChangeWrapper)
    	: this.store.subscribe(this.handleChangeWrapper)

    this.listeners = createListenerCollection()
  }
}

可以看到如果没有 unsubscribe 属性的话,会根据是否有 parentSub 属性进行下一步计算,这说明我们的 parentSub 是一个可选参数。如果没有 parentSub 的话就会直接使用 store.subscribe 来订阅 store 的更新,一旦数据更新,则会执行改类的 handleChangeWrapper 函数。如果有 parentSub 属性的话,就会执行 parentSub 的 addNestedSub 函数,因为这个函数存在于 Subscription 类上,所以可以猜想 parentSub 即为 Subscription 的一个实例。

在执行完 unsubscribe 的初始化之后,会初始化 listeners 的初始化,这里就用到了我们之前提到的那个工具函数。

我们看到订阅 store 更新的函数是 Subscription.prototype.handleChangeWrapper

handleChangeWrapper() {
  // this.onStateChange 由外部提供
  if (this.onStateChange) {
    this.onStateChange()
  }
}

而 onStateChange 函数在 Subscription 上并未被定义,只能说明这个函数在使用时被定义。等下我们阅读使用这个类的代码时可以看到。

我们再看看 Subscription 是如何使用我们的 listener 的:

addNestedSub(listener) {
  // 先执行 trySubscribe 函数,确定当前实例的订阅目标(parentSub or store)
  this.trySubscribe()
  // 子订阅都集中在 this.listeners 进行管理
  return this.listeners.subscribe(listener)
}

notifyNestedSubs() {
  this.listeners.notify()
}

addNestedSub 是 Subscription 的实例作为另一个 Subscription 实例的 parentSub 属性时被调用执行的函数,这个函数会把子 Subscription 实例的 handleChangeWrapper 函数注册到父 Subscription 实例的 listeners 中,当父 Subscription 实例调用 notifyNestedSub 时,所有的子 Subscription 的 handleChangeWrapper 函数都会被执行。

这就达到了一个目的,React Redux 通过 Subscription 和 listeners 可以构造一个 Subscription 实例构成的树,顶部的 Subscription 实例可以订阅 store 的变化,store 变化之后会执行 handleChangeWrapper 函数,而如果我们的 handleChangeWrapper 函数(内部执行 onStateChange)会调用 notifyNestedSub 函数的话,那不就所有的下层 Subscription 实例都会得到更新的消息?从而子 Subscription 实例的 handleChangeWrapper 函数就会被执行。这是一个由上而下的事件传递机制,确保了顶部的事件会被按层级先上后下的传递到下层。

示意图:

React-Redux 源码解析

通过这张示意图我们就可以很清楚的看到 Subscription 是如何实现事件由上而下派发的机制了。

回过头继续看我们如何实现组件订阅 Store 数据更新。

const [subscription, notifyNestedSubs] = useMemo(() => {
  if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY

  // This Subscription's source should match where store came from: props vs. context. A component
  // connected to the store via props shouldn't use subscription from context, or vice versa.
  // 如果 store 来自于最底层的 Provider,那么 parentSub 也要来自于 Provider
  const subscription = new Subscription(
    store,
    didStoreComeFromProps ? null : contextValue.subscription
  )

  // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
  // the middle of the notification loop, where `subscription` will then be null. This can
  // probably be avoided if Subscription's listeners logic is changed to not call listeners
  // that have been unsubscribed in the  middle of the notification loop.
  const notifyNestedSubs = subscription.notifyNestedSubs.bind(
    subscription
  )

  return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])

这是在计算当前组件用到的 subscription 实例和 notifyNestedSub 函数。

第一行代码标识如果用户配置了不处理 store 变化的话,就不需要 subscription 这个功能了。

后面开始初始化当前组件的 subscription 对象,如果 store 来自于 props,那么当前组件就是 Subscription 树最顶层的组件,它没有 parentSub,它直接订阅 store 的变化。

如果当前 store 来自于 context,那么表示它可能不是顶层的 Subscription 实例,需要考虑 contextValue 当中有没有包含 subscription 属性,如果有的话就需要将其作为 parentSub 进行实例化。

最后计算 notifyNestedSub 函数,之所以要绑定是因为像我之前在 Subscription 树状图中画的那样,这个函数要作为 subscription 实例的 handleChangeWrapper 函数调用,所以要确保 this 的指向不变。

这里容易产生一个疑问,为什么在 contextValue 中会有一个 subscription 实例传过来呢?我们在之前查看 Provider 组件源码的时候也没看到有这个属性呀。其实是后面的代码重写了传递给下层组件的 contextValue,确保下层被 connect 的组件能够拿到上层组件的 subscription 实例,达到构建 Subscription 树的目的:

// Determine what {store, subscription} value should be put into nested context, if necessary,
// and memoize that value to avoid unnecessary context updates.
const overriddenContextValue = useMemo(() => {
  if (didStoreComeFromProps) {
    // This component is directly subscribed to a store from props.
    // We don't want descendants reading from this store - pass down whatever
    // the existing context value is from the nearest connected ancestor.
    return contextValue
  }

  // Otherwise, put this component's subscription instance into context, so that
  // connected descendants won't update until after this component is done
  // 如果 store 来源于底层的 Provider,那么继续向下一层传递 subscription。
  return {
    ...contextValue,
    subscription
  }
}, [didStoreComeFromProps, contextValue, subscription])

这个 overriddenContextValue 属性就是被重写后的 contextValue,可以看到它把当前组件的 subscription 传到了下一层,这也就回到了 2.6 小节没有讲完的部分,也就是 ConnectFunction 的最后一段代码:

// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
  if (shouldHandleStateChanges) {
    // If this component is subscribed to store updates, we need to pass its own
    // subscription instance down to our descendants. That means rendering the same
    // Context instance, and putting a different value into the context.
    return (
      <ContextToUse.Provider value={overriddenContextValue}>
        {renderedWrappedComponent}
    	</ContextToUse.Provider>
    )
  }

  return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

return renderedChild

如果我们配置了需要处理 store 更新的话,就会重新使用 Provider 包裹下一层组件(即我们的 UI 组件),组件接受到的 contextValue 就是我们重写后的 contextValue:overriddenContextValue

所以下一层组件被 connect 之后,它的 ConnectFunction 就可以在 contextValue 中拿到它上一层组件的 subscription 对象,这样就将组件树关联起来啦,这是很重要的一步!

组件树之间的 subscription 树构建好之后,我们就需要看看他们之间是如何传递事件的。

在 ConnectFunction 内部,定义了一系列 ref:

// Set up refs to coordinate values between the subscription effect and the render logic
const lastChildProps = useRef()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false)

我们知道 React 的函数组件在每一次渲染的时候都拥有一个独立的上下文环境(不知道的童鞋可以阅读:精读《useEffect 完全指南》),为了防止每次 ConnectFunction 渲染拿不到上一次渲染的相关参数,我们需要 ref 来进行状态保留。

这里的四个 ref 保留了上一次 UI 组件渲染用到的 props、上一次的 wrapperProps 数据以及两个标志变量的内容。其中 childPropsFromStoreUpdate 代表由于 Store 更新导致的新的 props,renderIsScheduled 代表是否需要进行一次 rerender。

// 最终使用的 props
const actualChildProps = usePureOnlyMemo(() => {
  // Tricky logic here:
  // - This render may have been triggered by a Redux store update that produced new child props
  // - However, we may have gotten new wrapper props after that
  // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
  // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
  // So, we'll use the child props from store update only if the wrapper props are the same as last time.
  if (
    childPropsFromStoreUpdate.current &&
    wrapperProps === lastWrapperProps.current
  ) {
    return childPropsFromStoreUpdate.current
  }

  // TODO We're reading the store directly in render() here. Bad idea?
  // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
  // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
  // to determine what the child props should be.
  return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])

上面这是完整的 actualChildProps 的计算过程,不难发现其中关于 childPropsFromStoreUpdate 和 lastWrapperProps 的对比以及其注释。

这段代码的作用是如果在计算过程中由于 store 的更新导致新的 props 产生,并且当前 wrapperProps 没有发生变化,那么就直接使用新的 props,如果 wrapperProps 产生了变化的话就不能直接使用了,因为 wrapperProps 的变化可能导致计算的结果发生变化。

我们继续找到 subscription 实例的订阅代码:

// Actually subscribe to the nearest connected ancestor (or store)
// 订阅上层 subscription 的通知,当接受到通知时,说明 store state 发生了变化
// 需要判断是否 re-render,此时就会执行 checkForUpdates
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()

这里将当前 subscription 的 onStateChange 函数设置成了 checkForUpdates,如果当前 subscription 接收到了 store 更新的消息的话,就会执行 checkForUpdates 函数进行相关状态的更新以及 rerender。

那我们继续找到 checkForUpdates 函数的实现代码:

// We'll run this callback every time a store subscription update propagates to this component
// 每次收到 store state 更新的通知时,执行这个函数
const checkForUpdates = () => {
  if (didUnsubscribe) {
    // Don't run stale listeners.
    // Redux doesn't guarantee unsubscriptions happen until next dispatch.
    return
  }

  const latestStoreState = store.getState()

  let newChildProps, error
  try {
    // Actually run the selector with the most recent store state and wrapper props
    // to determine what the child props should be
    newChildProps = childPropsSelector(
      latestStoreState,
      lastWrapperProps.current
    )
  } catch (e) {
    error = e
    lastThrownError = e
  }

  if (!error) {
    lastThrownError = null
  }

  // If the child props haven't changed, nothing to do here - cascade the subscription update
  if (newChildProps === lastChildProps.current) {
    if (!renderIsScheduled.current) {
      notifyNestedSubs()
    }
  } else {
    // Save references to the new child props.  Note that we track the "child props from store update"
    // as a ref instead of a useState/useReducer because we need a way to determine if that value has
    // been processed.  If this went into useState/useReducer, we couldn't clear out the value without
    // forcing another re-render, which we don't want.
    lastChildProps.current = newChildProps
    childPropsFromStoreUpdate.current = newChildProps
    renderIsScheduled.current = true

    // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
    // 如果需要更新,执行 forceComponentUpdateDispatch 函数强制更新当前组件,这样就通过 subscription 完成了
    // state 的更新和组件的 re-render
    forceComponentUpdateDispatch({
      type: 'STORE_UPDATED',
      payload: {
        error
      }
    })
  }
}

可以看到 store 更新事件到达后,会先计算出一个 newChildProps,即我们新的 props,过程中如果有计算错误会被保存。

如果计算出来的 props 和当前 lastChildProps 引用的结果一致的话,说明数据没有发生变化,组件如果没有更新计划的话就需要手动触发 notifyNestedSubs 函数通知子组件更新。

如果计算出来的 props 和之前的 props 不相等的话,说明 store 的更新导致 props 发生了变化,需要更新相关引用,并触发当前组件更新,当前组件更新后 ConnectFunction 的相关计算又开始了新的一轮,所以又回到了我们之前讲的 actualChildProps 数据的计算。这也是为什么在 actualChildProps 的计算过程中还要考虑 props 和 wrapperProps 的更新。

我们看到 checkForUpdates 更新当前组件是调用了 forceComponentUpdateDispatch 函数,我们看看其实现:

function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]
}

// We need to force this wrapper component to re-render whenever a Redux store update
// causes a change to the calculated child component props (or we caught an error in mapState)
// 通过 useReducer 强制更新当前组件,因为每次 dispatch 之后 state 都会发生变化
// storeStateUpdatesReducer 返回的数组的第二个参数会一直增加
const [
  [previousStateUpdateResult],
  forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

// 如果下层组件在使用时有捕获到错误,则在当前这层抛出
// Propagate any mapState/mapDispatch errors upwards
if (previousStateUpdateResult && previousStateUpdateResult.error) {
  throw previousStateUpdateResult.error
}

这里通过 useReducer Hooks 构造了一个数据和一个更新函数。

我们看到 reducer 是一个每次都会固定更新的函数(updateCount 永远自增),所以每次调用 forceComponentUpdateDispatch 都会导致当前组件重新渲染。而其数据中 error 的来源就是我们 checkForUpdates 计算下一次 props 的时候捕捉到的错误:

forceComponentUpdateDispatch({
  type: 'STORE_UPDATED',
  payload: {
    error
  }
})

如果在计算过程中产生了错误,会在下一次渲染的时候抛出来。

我们需要在组件渲染之后更新之前的引用,所以会有下面这段代码:

 // We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
// 通过使用 useLayoutEffect 实现在 DOM 更新之后做一些操作,这里是在 DOM 更新之后更新内部的一些 ref
// 确保下一次判断时使用的 ref 是最新的
useIsomorphicLayoutEffect(() => {
  // We want to capture the wrapper props and child props we used for later comparisons
  lastWrapperProps.current = wrapperProps
  lastChildProps.current = actualChildProps
  renderIsScheduled.current = false

  // If the render was from a store update, clear out that reference and cascade the subscriber update
  if (childPropsFromStoreUpdate.current) {
    childPropsFromStoreUpdate.current = null
    notifyNestedSubs()
  }
})

同时可以看到在渲染完成之后,会通知所有子组件 store 数据发生了更新。

值得注意的是 useIsomorphicLayoutEffect 这个自定义 Hook:

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed

// 参考:https://reactjs.org/docs/hooks-reference.html#uselayouteffect

export const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' &&
  typeof window.document !== 'undefined' &&
  typeof window.document.createElement !== 'undefined'
    ? useLayoutEffect
    : useEffect

由于 useLayoutEffect 不适用于 SSR 的场景,所以会使用 useEffect 作为 fallback。

四、结语

讲到这里,React Redux 的工作原理基本就解析完了,文章中为了避免出现大段的代码已经尽量少的粘贴源码,所以可能会导致阅读起来会存在一定困难。建议大家去我的 GitHub 上面看源码,包含有相关注释,有什么问题也可以在 issues 提出哦。

分类:

技术点:

相关文章: