阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates

终于到了最核心的connectAdvanced.js文件,在这里做的最主要的事情就是响应Provider提供的store的改变。除了响应store的变化,还做了很多事情,分别是:ref的处理,是否是pure模式,还有对于store改变的事件的转发。

首先回顾下connectAdvanced函数在connect.js中是如何被使用的。

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函数调用后返回connectHOC调用后的返回值

而connect函数是这样用的:

connect(mapStateToProps, mapDispatchToProps)(wrappedComponent)

connect的返回值就是connectHOC的返回值,所以返回值一定也是一个方法。connectHOCReturnValue(wrappedComponent)

connectAdvanced

继续来看connectAdvanced.js文件中connectAdvanced方法的实现。因为整个方法的实现异常复杂,所以我决定分开叙述,以功能点的方式进行,首先最重要的当然是如何响应store中的变化

function connectAdvanced (
	selectorFactory,
   {
    
    
    // the func used to compute this HOC's displayName from the wrapped component's displayName.
    // probably overridden by wrapper functions such as connect()
    getDisplayName = name => `ConnectAdvanced(${
      
      name})`,

    // shown in error messages
    // probably overridden by wrapper functions such as connect()
    methodName = 'connectAdvanced',

    // REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
    // calls to render. useful for watching in react devtools for unnecessary re-renders.
    renderCountProp = undefined,

    // determines whether this HOC subscribes to store changes
    shouldHandleStateChanges = true,

    // REMOVED: the key of props/context to get the store
    storeKey = 'store',

    // REMOVED: expose the wrapped component via refs
    withRef = false,

    // use React's forwardRef to expose a ref of the wrapped component
    forwardRef = false,

    // the context consumer to use
    context = ReactReduxContext,

    // additional options are passed through to the selectorFactory
    ...connectOptions
  } = {
    
    }
) {
    
    
  ...
  return function wrapWithConnect(WrappedComponent) {
    
    
    ...
  }
}

wrapWithConnect函数就是connectAdvanced函数的返回值,也就是 connect(mapStateToProps, mapDispatchToProps)的返回值,入参WrappedComponent就是我们的业务组件(需要连接到store的组件)。

wrapWithConnect

return function wrapWithConnect(WrappedComponent) {
    
    
  
  ...
  
  const selectorFactoryOptions = {
    
    
    ...connectOptions,
    getDisplayName,
    methodName,
    renderCountProp,
    shouldHandleStateChanges,
    storeKey,
    displayName,
    wrappedComponentName,
    WrappedComponent
  }
  
  ...
  
  function createChildSelector(store) {
    
    
    return selectorFactory(store.dispatch, selectorFactoryOptions)
  }
  
  ...
  
  function ConnectFunction(props) {
    
    
    ...
  }
    
  const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
    
  ...
  
  return hoistStatics(Connect, WrappedComponent)
}

函数wrapWithConnect的入参是我们的业务组件,返回值是一个内部组件 Connect,return hoistStatics(Connect, WrappedComponent)hoistStatics函数主要作用是复制函数的静态属性,本例中是将WrappedComponent的静态属性复制到Connect组件上(Connect就是 ConnectFunction)。

核心中的核心 ConnectFunction 函数

通过其大写的首字母就知道是一个React组件。这个组件就是真正被导出的组件。看看是怎么实现的,如何连接到store的。

function ConnectFunction(props) {
    
    
  
  ...
  
  const contextValue = useContext(ContextToUse)
  
  ...
  
  const store = didStoreComeFromProps ? props.store : contextValue.store
  
  ...
  
  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])
  
  ...
  
  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.
    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])
  
  ...
  
  const [
    [previousStateUpdateResult],
    forceComponentUpdateDispatch
  ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
  
  if (previousStateUpdateResult && previousStateUpdateResult.error) {
    
    
    throw previousStateUpdateResult.error
  }
  
  // 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)
  
  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])

  // 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.
  useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs
  ])
  
  // Our re-subscribe logic only runs when the store/subscription setup changes
  useIsomorphicLayoutEffectWithArgs(
    subscribeUpdates,
    [
      shouldHandleStateChanges,
      store,
      subscription,
      childPropsSelector,
      lastWrapperProps,
      lastChildProps,
      renderIsScheduled,
      childPropsFromStoreUpdate,
      notifyNestedSubs,
      forceComponentUpdateDispatch
    ],
    [store, subscription, childPropsSelector]
  )

  // 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]
	)

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

首先从contextValue中取出store。然后拿到childPropsSelector(用于计算正真需要注入业务组件的完整props),该函数通过store.getState()wrapperProps来计算出新的actualChildProps用于渲染子组件。

再往下看得到了[subscription, notifyNestedSubs]这两个名字应该很熟悉了在阅读react-redux源码 - 一中有详细的介绍。这里的subscripion也是一个事件对象,而notifyNestedSubs可以通知subscription的所有监听者,事件发生,执行回调。

const subscription = new Subscription(
  store,
  didStoreComeFromProps ? null : contextValue.subscription
)

这里的subscription监听了contextValue.subscription的变化,或者store的变化(这里后面详细介绍)。

const [
  [previousStateUpdateResult],
  forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

这个是引起组件 ConnectFunction更新的关键,只有调用forceComponentUpdateDispatch函数,组件ConnectFunction才会更新。

storeStateUpdatesReducer

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

storeStateUpdatesReducer直接返回一个数组,第一项就是dispatch的action的payload,也就是说 forceComponentUpdateDispatch 入参的payload属性是什么那么previousStateUpdateResult值就是什么。

如果previousStateUpdateResult.error有值表示发生错误,直接抛出去,不要继续了。

if (previousStateUpdateResult && previousStateUpdateResult.error) {
    
    
  throw previousStateUpdateResult.error
}

下面定义了几个ref:

const lastChildProps = useRef()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false)

lastChildProps:最后的childProps(传递给WrappedComponent的props,这里记录的是上一次的props)

lastWrapperProps:父元素传递进来的props

childPropsFromStoreUpdate:store更新计算出来的childProps

renderIsScheduled:是否在render中

下面开始计算真实的childProps也就是actualChildProps值:

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

首先查看是否是store变动引起的更新,如果是,还需要查来自父元素的props是否没有更新过,如果是,则直接返回store更新计算出来的childProps,否则弃用store更新计算出来的childProps(childPropsFromStoreUpdate.current)重新通过childPropsSelector(store.getState(), wrapperProps)计算childProps。

 useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
   lastWrapperProps,
   lastChildProps,
   renderIsScheduled,
   wrapperProps,
   actualChildProps,
   childPropsFromStoreUpdate,
   notifyNestedSubs
 ])

这个其实就是在缓存数据,为了缓存本轮更新的值,在下一次更新的时候可以拿到现在的值。

captureWrapperProps

function captureWrapperProps(
  lastWrapperProps,
  lastChildProps,
  renderIsScheduled,
  wrapperProps,
  actualChildProps,
  childPropsFromStoreUpdate,
  notifyNestedSubs
) {
    
    
  // 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()
  }
}

这里做了两件事情,一个是缓存了wrapperProps值,actualChildProps值和将是否在渲染重置为false。第二个是检查是否是store变更引起的更新,如果是则通知subscription的订阅者需要拉取最新的state。

在往下就实现了订阅更新了,关联起上面的subscription实例和上面提到的更新组件的唯一方法 forceComponentUpdateDispatch

function subscribeUpdates(
  shouldHandleStateChanges,
  store,
  subscription,
  childPropsSelector,
  lastWrapperProps,
  lastChildProps,
  renderIsScheduled,
  childPropsFromStoreUpdate,
  notifyNestedSubs,
  forceComponentUpdateDispatch
) {
    
    
  // If we're not subscribed to the store, nothing to do here
  if (!shouldHandleStateChanges) return

  // Capture values for checking if and when this component unmounts
  let didUnsubscribe = false
  let lastThrownError = null

  // We'll run this callback every time a store subscription update propagates to this component
  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({
    
    
        type: 'STORE_UPDATED',
        payload: {
    
    
          error
        }
      })
    }
  }

  // Actually subscribe to the nearest connected ancestor (or store)
  subscription.onStateChange = checkForUpdates
  subscription.trySubscribe()

  // Pull data from the store after first render in case the store has
  // changed since we began.
  checkForUpdates()

  const unsubscribeWrapper = () => {
    
    
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null

    if (lastThrownError) {
    
    
      // It's possible that we caught an error due to a bad mapState function, but the
      // parent re-rendered without this component and we're about to unmount.
      // This shouldn't happen as long as we do top-down subscriptions correctly, but
      // if we ever do those wrong, this throw will surface the error in our tests.
      // In that case, throw the error from here so it doesn't get lost.
      throw lastThrownError
    }
  }

  return unsubscribeWrapper
}

这个函数里主要是将 subscription 的change关联到forceComponentUpdateDispatch上,实现方式如下:

subscription.onStateChange = checkForUpdates
subscription.trySubscribe()

checkForUpdates()

checkForUpdates

首先根据最新的state和最新的wrapperProps来计算出newChildProps,如果计算出来的childProps和上一次的childProps一样,那么当前组件不必更新。但是当组件不需要更新的时候则需要单独通知subscription的监听者,state是有更新的,因为每个组件监听的state是不一样的,虽然当前组件没有更新,但是别的组件会获取到新的state用以更新。

如果不一样则需要更新:

lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true

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

需要更新当前组件需要调用方法forceComponentUpdateDispatch,并且设置缓存上lastChildProps.current = newChildPropschildPropsFromStoreUpdate.current = newChildProps,其中childPropsFromStoreUpdate.current会在 forceComponentUpdateDispatch一起的下一次更新时候通知captureWrapperProps函数需要notifyNestedSubs,通知subscription对象有state更新。

到这里说完了store更新的流程:store更新会触发subscription的onStateChange也就是上面说的checkForUpdates方法,该方法会检查当前组件订阅的state和wrapperProps生成的childProps是否改变了,如果改变需要通知更新当前组件,如果没有改变需要通知自己的订阅者有新的state产生。

#wrapperProps更新引起的组件更新

const actualChildProps = usePureOnlyMemo(() => {
    
    
  if (
    childPropsFromStoreUpdate.current &&
    wrapperProps === lastWrapperProps.current
  ) {
    
    
    return childPropsFromStoreUpdate.current
  }
  
  return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])

父组件传入的props更新的话会重新计算actualChildProps,然后传给组件WrappedComponent。

单独由父组件传入的props更新导致的组件更新,childPropsFromStoreUpdate.current 值一定为假所以执行的是 childPropsSelector(store.getState(), wrapperProps)计算新的 actualChildProps

当然wrapperProps的更新还会执行函数captureWrapperProps来捕获新的值,用于下次对比。

renderIsScheduled

这个标识符表示的是否在更新中,我们知道react的update是“异步”的,所以当连续多次dispatch的时候:

newChildProps === lastChildProps.current这个等式可能会多次成立(dispatch(1); dispatch(1);)。如果没有 !renderIsScheduled.current控制会导致 notifyNestedSubs()多次执行,这是不必要的,因为后面的:

function captureWrapperProps() {
    
    
  
  ...
  
  if (childPropsFromStoreUpdate.current) {
    
    
    childPropsFromStoreUpdate.current = null
    notifyNestedSubs()
  }
  
  ...
  
}

更新结束后会一次性通知有state改变。

继续往下看:

function ConnectFunction(props) {
    
    
  
  ...
  
  const renderedWrappedComponent = useMemo(
    () => <WrappedComponent {
    
    ...actualChildProps} ref={
    
    forwardedRef} />,
    [forwardedRef, WrappedComponent, actualChildProps]
  )

  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
}

ConnectFunction组件最后返回了组件renderedChild,而该组件则是renderedWrappedComponent是一个被缓存的组件:<WrappedComponent {...actualChildProps} ref={forwardedRef} />可以看出来actualChildProps这个计算出来的总属性被注入给了我们的业务组件。

以上就是从订阅store更新以更新组件自身的角度看了函数connectAdvanced,接下来将从生下的角度查看该函数做的事情。

猜你喜欢

转载自blog.csdn.net/letterTiger/article/details/107305480
今日推荐