初探 react-redux

·  阅读 662

js

作为一个初学者刚开始接触 redux 时感觉是那么的神奇,管它三七二十八直接看 api 干就得了,直到后来接触了高阶组件(HOC)听名字是不是特别高大上,到底有多高呢?2米?3米?反正我没拿尺测量过。到这才知道原来我一直在用的 connect 就是一个高阶组件,那这个 connect 又是怎么实现的呢,管它呢,会用就好了啊,对吧。那个时候资深技术专家的键盘 ctrl + c、ctrl + v 三个键我用的那叫一个炉火纯青啊...

在之后的面试中考官问:用过 redux 吗?当然用过啊,so easy!那请问 connect 是怎么实现的呢?然后我一脸懵,毕竟 ctrl + c + v 啊...灰溜溜的 gun 了,不带走一片云彩~~~ 唉,猿路难走啊

言归正传,我们接下来看看 react-redux 中主要的几个 api 实现过程吧,基于 7.2.1 版本

react-redux 的核心机制无非就是订阅和通知,源码中有一个 Subscription 类,它的作用主要是订阅父级的更新和通知子级的更新。最外层的 Provider 组件的 Context 里包含了 store 和生成的 Subscription 实例,Subscription 实例订阅的则是 redux 的 subscrib()。当我们使用了 connect() 时,它会生成一个新组件,新组件里会生成一个 Subscription 实例,它会订阅父级的 Subscription 实例,同时将自己的 Subscription 覆盖进 Context,再包装我们传入的组件

// 通过 connect 函数包装过后
// overriddenContextValue 包含了新组件的 Subscription 实例和 store
<WrapComponent.Provider value={overriddenContextValue}>
  {WrappedComponent}
</WrapComponent.Provider>
复制代码

源码简析

react-redux 导出以下 api 供使用

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

Subscription

// 默认的监听器
const nullListeners = { notify() {} }

// 这是一个返回纯对象的函数
// 这个对象的作用就是一个事件发布订阅中心,这是属于观察者模式的应用,我们的所有 listener 都会监听当前对象,一旦当前对象调用 notify,所有 listener 都会被执行。
// 这个对象的 subscribe 或者 notify 的调用时机取决于该对象的使用者
function createListenerCollection() {
  const batch = getBatch()
  let first = null
  let last = null

  return {
    clear() {
      first = null
      last = null
    },

    // 通知订阅者
    notify() {
      batch(() => {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },

    // 注册监听器
    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    // 订阅逻辑
    subscribe(callback) {
      let isSubscribed = true

      // 把 last 赋值为新的
      let listener = (last = {
        callback,
        next: null,
        prev: last
      })

      // 如果存在前一个,就把前一个的 next 指向当前(最后一个)
      if (listener.prev) {
        listener.prev.next = listener
      } else {
        // 否则它就是第一个
        first = listener
      }

      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    }
  }
}

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

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  // addNestedSub 是 Subscription 的实例作为另一个 Subscription 实例的 parentSub 属性时被调用执行的函数
  // 这个函数会把子 Subscription 实例的 handleChangeWrapper 函数注册到父 Subscription 实例的 listeners 中
  // 当父 Subscription 实例调用 notifyNestedSub 时,所有的子 Subscription 的 handleChangeWrapper 函数都会被执行。
  addNestedSub(listener) {
    // 先执行 trySubscribe 函数,确定当前实例的订阅目标(parentSub or store)
    this.trySubscribe()
    // 子订阅都集中在 this.listeners 进行管理
    return this.listeners.subscribe(listener)
  }

  // 通知子级
  notifyNestedSubs() {
    this.listeners.notify()
  }

  handleChangeWrapper() {
    // onStateChange 是 new 出实例的时候加上的
    if (this.onStateChange) {
      this.onStateChange()
    }
  }

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

  trySubscribe() {
    // 如果没有 unsubscribe 属性的话,会根据是否有 parentSub 属性进行下一步计算,这说明我们的 parentSub 是一个可选参数
    // 如果没有 parentSub 的话就会直接使用 store.subscribe 来订阅 store 的更新,一旦数据更新,则会执行改类的 handleChangeWrapper 函数。
    // 如果有 parentSub 属性的话,就会执行 parentSub 的 addNestedSub 函数,因为这个函数存在于 Subscription 类上,所以可以猜想 parentSub 即为 Subscription 的一个实例
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        // subscribe 是 redux 里的方法,在 redux state 改变的时候会调用
        : this.store.subscribe(this.handleChangeWrapper)

      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}
复制代码

附上一张订阅图

Subscription

Provider

function Provider({ store, context, children }) {
  // 使用 useMemo 做优化仅在 store 变化时再重新计算 contextValue
  const contextValue = useMemo(() => {
    // 实例化订阅器
    const subscription = new Subscription(store)
    // 通知订阅这个 subscription 的子级刷新
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      // redux store
      store,
      // 将此 subscription 传入 context 方便子级订阅
      subscription
    }
  }, [store])

  // 缓存上次的 store state
  const previousState = useMemo(() => store.getState(), [store])

  // 副作用生命周期,仅在 contextValue, previousState 变化时执行
  useEffect(() => {
    const { subscription } = contextValue
    // 在这里是订阅的 reudx store 的 subscribe 事件,注册监听器
    subscription.trySubscribe()

    // 如果 store state 改变,通知子级监听
    if (previousState !== store.getState()) {
      subscription.notifyNestedSubs()
    }
    
    // 卸载后重置
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  // 传入的 context 或者 react 自带的 Context
  const Context = context || ReactReduxContext

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
复制代码

Provider是一个比较简单的组件,主要做了2件事:

  • 订阅 redux 的 subscribe() 事件

  • 将 Subscription 实例传入 Context 方便子级订阅

connect

connect 函数来源于 createConnect 函数调用,为什么需要 createConnect 函数呢?它的作用简单来说是使用默认的参数产生 connect 函数。

源码里面有一段注释也对 connect 和 connectHOC 得关系解释得非常清楚,大家可以去看看。大致意思是 connect 将其 args 作为选项传递给 connectAdvanced(connectHOC),每次实例化或热重载 Connect 组件实例时,它们都会依次将它们传递给 selectorFactory。

// 遍历执行函数,它也是一个校验器
// 第一个参数是我们传入的 mapStateToProps,mapDispatchToProps、mergeProps
// 第二个参数是一个数组(之后会介绍每一个数组元素)
// 第三个参数是对应的函数名称
function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return ...(执行校验)
}

...

// 使用默认参数调用 createConnect 生成我们所熟悉的 connect 函数
export function createConnect({
  // 默认值,之后会一一介绍
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
    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
    })
  }
}

export default /*#__PURE__*/ createConnect()
复制代码

可以看到 connect 函数只是 connectAdvanced 函数的一个代理人,它所做的工作就是将我们传入的参数转化为可以供 selectorFactory 使用的参数。而可以供 selectorFactory 使用的符合如下定义的标准结构

(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps
复制代码

也可以看到我们传入的 mapStateToProps,mapDispatchToProps、mergeProps 实际上是会通过一个 match() 函数进行包装校验

mapStateToPropsFactories

// 当传入了 mapStateToProps 时,经过 wrapMapToPropsFunc 包装后返回
export function whenMapStateToPropsIsFunction(mapStateToProps) {
  return typeof mapStateToProps === 'function'
    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
    : undefined
}

// 当没有传入 mapStateToProps 时,经过 wrapMapToPropsFunc 包装后返回一个默认值
export function whenMapStateToPropsIsMissing(mapStateToProps) {
  return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
}

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
复制代码

在 match 校验的时候,首先会判断我们是否传入的 mapStateToProps,没有传入则调用 wrapMapToPropsConstant 创建一个默认方法。如果传入则会调用 wrapMapToPropsFunc 对我们的方法做一层包装,我们再深入看看 wrapMapToPropsConstant 和 wrapMapToPropsFunc 这两个函数

wrapMapToPropsConstant

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

    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}
复制代码

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

wrapMapToPropsFunc

// 该函数用于计算 mapToProps 是否需要使用到 props
// 依据是 function.length,如果 function.length === 1 就说明只需要 stateOrDispatch,如果 function.length !== 1,说明就需要 props 进行计算。

// 当 mapToProps.dependsOnOwnProps 有值时就直接使用这个值作为结果,不再重新计算
// 如果还是没有值的话, 需要进行一次计算
// 计算的逻辑就是 mapToProps 函数的参数格式,即我们传递给 connect 函数的 mapStateToProps 函数的参数个数,只有当参数个数为 1 的时候才不会传入 ownProps 参数。
export function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null &&
    mapToProps.dependsOnOwnProps !== undefined
    ? Boolean(mapToProps.dependsOnOwnProps)
    : mapToProps.length !== 1
}

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

    // 根据 dependsOnOwnProps 的值决定了我们在调用 proxy.mapToProps 函数时是否传入第二个参数
    // 默认为 true,这是为了让 detectFactoryAndVerify 函数执行时可以得到 ownProps 这个参数
    proxy.dependsOnOwnProps = true
    
    // 第一次运行时计算 dependsOnOwnProps 的值、校验返回的 props 结果等工作
    // 重新为 mapToProps 赋值(意味着第二次运行 proxy.mapToProps 时就不会重新计算 dependsOnOwnProps 的值和校验返回的 props 结果了)
    proxy.mapToProps = function detectFactoryAndVerify(
      stateOrDispatch,
      ownProps
    ) {
      // 重新为 mapToProps 赋值
      proxy.mapToProps = mapToProps
      
      // 计算 dependsOnOwnProps 的值
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      
      // 返回得结果是标准格式中的 nextFinalProps
      let props = proxy(stateOrDispatch, ownProps)

      // 如果我们的 mapStateToProps 返回的结果是一个函数,则在后续的计算中,这个返回的函数会作为真正的 mapToProps 函数进行 props 的计算
      // 也就是说传入的 mapToProps 除了可以返回一个纯对象以外,还可以返回一个函数。
      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      // 校验返回的 props 结果
      if (process.env.NODE_ENV !== 'production') {
        verifyPlainObject(props, displayName, methodName)
      }
      
      return props
    }

    return proxy
  }
}
复制代码

mapDispatchToPropsFactories

import { bindActionCreators } from 'redux'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'

// mapDispatchToProps 为 func 的情况
export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === 'function'
    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
    : undefined
}

// mapDispatchToProps 为 null 的情况
export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return !mapDispatchToProps
    ? wrapMapToPropsConstant(dispatch => ({ dispatch }))
    : undefined
}

// mapDispatchToProps 为 object 的情况
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. 当传入的 mapDispatchToProps 为对象时,说明这是一个 ActionCreator 对象,可以通过使用 redux 的 bindActionCreator API 将这个 ActionCreator 转化为包含很多函数的对象并 merge 到 props.

mergePropsFactories

import verifyPlainObject from '../utils/verifyPlainObject'

// 直接对 stateProps、dispatchProps 和 ownProps 三者的合并并返回
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

// 这种情况很少遇到,使用 connect 时基本都不会传第三个 mergeProps 参数
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) {
        if (!pure || !areMergedPropsEqual(nextMergedProps, mergedProps))
          mergedProps = nextMergedProps
      } else {
        hasRunOnce = true
        mergedProps = nextMergedProps

        if (process.env.NODE_ENV !== 'production')
          verifyPlainObject(mergedProps, displayName, 'mergeProps')
      }

      return mergedProps
    }
  }
}

// mergeProps 是一个函数时
export function whenMergePropsIsFunction(mergeProps) {
  return typeof mergeProps === 'function'
    ? wrapMergePropsFunc(mergeProps)
    : undefined
}

// mergeProps 省略时
export function whenMergePropsIsOmitted(mergeProps) {
  return !mergeProps ? () => defaultMergeProps : undefined
}

export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
复制代码

基本上就是直接对 stateProps、dispatchProps 和 ownProps 三者的合并,加上了一些基本的校验。

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

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

selectorFactory

export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  // 可以看到一开始通过 init... 系列函数计算出了需要的 mapStateToProps 、mapDispatchToProps 和 mergeProps 函数
  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
    )
  }

  // 根据 options.pure 的值选择不同的函数进行下一步计算
  // React Redux 源码不得不提的一个点就是配置项中的 pure 参数,我们可以在 createStore 的时候传入该配置,该配置默认为 true
  // 当 pure 为 true 的时候,React Redux 内部有几处地方就会针对性地进行优化,比如我们这里看到的 selectFactory。当 pure 为不同的值时选择不同的函数进行 props 的计算。如果我们的 pure 为 false,则每次都进行相应计算产生新的 props,传递给我们的内部组件,触发 rerender
  // 当我们的 pure 为 true 的时候,React Redux 会缓存上一次计算的相应结果,然后在下一次计算后对比结果是否相同,如果相同的话就会返回上一次的计算结果,一旦我们的组件是纯组件,则传入相同的 props 不会导致组件 rerender,达到了性能优化的目的  
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
复制代码

impureFinalPropsSelectorFactory

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

// 这样每次计算都会返回新的 props,导致组件一直 rerender
export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}
复制代码

pureFinalPropsSelectorFactory

当 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)
    
    // 第一次计算时的过程跟 impureFinalPropsSelectorFactory 一致,但是多了个闭包内缓存的过程
    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
  }

  // 随后的 props 计算当中,会根据 state 和 props 的变化情况选择不同的函数进行计算,这样做是为了尽可能的减少计算量,优化性能。
  // 如果 state 和 props 都没有发生变化的话,就直接返回缓存的 props
  function handleSubsequentCalls(nextState, nextOwnProps) {
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps

    // 如果 props 和 state 都改变时,调用 handleNewPropsAndNewState 函数
    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    
    // 如果 props 改变时,调用 handleNewProps 函数
    if (propsChanged) return handleNewProps()
    
    // 如果 stateChanged 改变时,调用 handleNewState 函数
    if (stateChanged) return handleNewState()
    
    // 如果 props 和 state 都没改变时,直接返回 mergedProps
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}
复制代码

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

{
  pure = true,
  areStatesEqual = strictEqual,
  areOwnPropsEqual = shallowEqual,
  areStatePropsEqual = shallowEqual,
  areMergedPropsEqual = shallowEqual,
  ...extraOptions
} = {}
复制代码

strictEqual

function strictEqual(a, b) {
  return a === b
}
复制代码

shallowEqual

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 的源码中仍然可以看到许多针对性的检查。

我们其实可以发现 selectorFactory 确实符合之前提到的标准格式:

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

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

connectAdvanced

打开 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])
复制代码

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

ConnectFunction

const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
  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(() => {
  return propsContext &&
    propsContext.Consumer &&
    isContextConsumer(<propsContext.Consumer />)
    ? propsContext
    : Context
}, [propsContext, Context])

// 通过 useContext 使用 context,并且订阅其变化,当 context 变化时会 re-render。
const contextValue = useContext(ContextToUse)

// 检查 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)
// 使用传入的 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 对象。

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

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 组件了:

const renderedWrappedComponent = useMemo(
  () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
  [forwardedRef, WrappedComponent, actualChildProps]
)
复制代码

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

renderedWrappedComponent 组件就是我们渲染 UI 组件的结果,然而我们还不能直接拿去返回给用户渲染,我们还要考虑其他情况,UI 组件是如何订阅 store 更新的参考一开始介绍的 Subscription 类。

写在最后

猿路难走,且行且珍惜~~~~~~

分类:
前端
标签:
分类:
前端
标签: