阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理

阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates中介绍了函数connectAdvanced是如何关联到store变动的,其实这个函数做的事情不仅仅有关联变动,还有对于store改变的事件转发、ref的处理和pure模式的处理。

首先来看store改变的事件的转发。

store改变的事件转发

function connectAdvanced (
	selectorFactory,
  {
    
    
    ...
    context = ReactReduxContext,
    ...
  }
) {
    
    
  ...

  const Context = context

  ...
  
  return function wrapWithConnect (WrappedComponent) {
    
    
    ...
    
    function ConnectFunction (props) {
    
    
      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])
      
      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])
      
      // 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.
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)
      
      if (
        process.env.NODE_ENV !== 'production' &&
        !didStoreComeFromProps &&
        !didStoreComeFromContext
      ) {
    
    
        throw new Error(
          `Could not find "store" in the context of ` +
            `"${
      
      displayName}". Either wrap the root component in a <Provider>, ` +
            `or pass a custom React context provider to <Provider> and the corresponding ` +
            `React context consumer to ${
      
      displayName} in connect options.`
        )
      }

      // Based on the previous check, one of these must be true
      const store = didStoreComeFromProps ? props.store : contextValue.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])

      // 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
        return {
    
    
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])
    }
    
    ...
    
    // 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
  }
  
  const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
    
  ...
  
  return hoistStatics(Connect, WrappedComponent)
  
}

可以看出来ConnectFunction组件中如果props传入了合法的context那么ContextToUse就是props.context如果不是则使用默认的ReactReduxContext。

到这里拿到了ContextToUse,后面通过useContext(ContextToUse)得到context中传过来的值contextValue。通过前面的解读我们知道contextValue需要一个对象中包含两个值,一个是store一个是subscription(Subscription的实例)。

除了context可以使用来自props外store也可以来自props。如果props中有store那么就放弃contextValue中的store而使用props中传入的store。

如果contextValue中也没有store,毕竟contextValue可能来自用户传入,props中也没有store,没有store监听啥,继续不下去了,直接报错。

根据store和contextValue和didStoreComeFromProps三个值来生成新的subscription(不明白为啥要生成新的,直接订阅传入的subscription不是更加简单吗?如果是props.store再生成新的)。得到新的subscription和subscription的通知函数notifyNestedSubs,来通知subscription的订阅者。

重写contextValue使用新生成的subscription来覆盖contextValue中的subscription,然后通过Provider提供给后代组件。

小结:整个react-redux构建的上下文中,并不只能有位移的store和context,还可以通过props的方式传入别的store和context让Connect组件响应这个传入的store和context的变动。

获取业务组件的实例(透传ref)

function createConnect() {
    
    
  return function connect() {
    
    
    return function connectHOC()
  }
}
  
export default createConnect()
// 就是上面的connectHOC
function connectAdvanced () {
    
    
  return function wrapWithConnect(WrappedComponent) {
    
    
    function ConnectFunction () {
    
    
      return <WrappedComponent />
    }
    return ConnectFunction
  }
}

基本上connect(mapStateToProps, mapDispatchToProps)(wrappedComponent)就是这个结构,所以最后被返回的出去的是ConnectFunction这个组件,如果在connectFunction上设置props ref也拿不到业务组件WrappedComponent的实例。

首先在connectAdvanced的配置项中有forwardRef = false如果是true,那么connectAdvanced就会帮你透传ref给业务组件。

看看如何做的:

function connectAdvanced(
	selectorFactory,
  {
    
    
    forwardRef = false
  }
) {
    
    
    return function wrapWithConnect(WrappedComponent) {
    
    
      function ConnectFunction () {
    
    
        const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
    
    
        	const {
    
     forwardedRef, ...wrapperProps } = props
        	return [forwardedRef, wrapperProps]
      	}, [props])
        
        return <WrappedComponent ref={
    
    forwardedRef} />
      }
      
    	return ConnectFunction
    

      if (forwardRef) {
    
    
        const forwarded = React.forwardRef(function forwardConnectRef(
          props,
          ref
        ) {
    
    
          return <ConnectFunction {
    
    ...props} forwardedRef={
    
    ref} />
        })

        return forwarded
      }

    	return ConnectFunction
  }
}

上面的代码基本上就是透传ref的所有实现了,和源码有些出入,主要是删除了一些干扰因素。

如果配置项中forwardRef是true那么wrapWithConnect返回的就是:

React.forwardRef(
  function forwardConnectRef(props, ref) {
    
    
 		return <ConnectFunction {
    
    ...props} forwardedRef={
    
    ref} />
	}
)

使用React.forwardRef提取了ref通过keyforwardedRef属性传递给了ConnectFunction组件。connectFunction组件内部拿到forwardedRef只有再给到业务组件<ConnectFunction ref={forwardedRef} />。这样就完成了透传ref的任务,让父组件可以拿到被包裹的业务组件的实例。

pure模式

对于PureComponent我们有所了解,当props变动的时候这个Component会帮助我们自动做一个props的浅比较,如果浅比较相等则不会update组件,如果不相等才会update组件。

对于类组件可以继承PureComponent而函数式组件可以使用React.memo来做同样的事情,当然钩子函数useMemo也可以达到同样的效果。

业务组件的props有两个来源,一个是父组件,一个是store。store又被分为两个,一个是store的state,一个是store的dispatch。

想要让整个业务组件是pure那么就要考虑两个方面的变动,一个是父组件重新渲染的时候需要浅比较当前业务子组件的props来阻止不必要的更新,另一个是store中state更新的时候,需要对当前组件监听的state做浅比较防止不必要的更新。

return function wrapWithConnect(WrappedComponent) {
    
    
  
  ...
  
  const {
    
     pure } = connectOptions
  
  ...
  
  const usePureOnlyMemo = pure ? useMemo : callback => callback()
  
  ...
  
  function ConnectFunction(props) {
    
    
    ...
    
    const actualChildProps = usePureOnlyMemo(() => {
    
    
      if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
    
    
          return childPropsFromStoreUpdate.current
        }
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])
    
    ...
    
  }
    
  ...
  
  const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
    
  ...
}


首先拿到配置中的pure,根据pure得到usePureOnlyMemo,如果是pure模式那么actualChildProps的计算就会被memo,如果依赖没变则不会重新计算。

如果是pure模式的话Connect组件则等于React.memo(ConnectFunction),这样会阻止掉父组件更新导致的子组件不必要的更新,作用和PureComponent一样,会进行一次属性的浅对比,相等则不更新当前组件。

接下来看store更新是如何避免不必要的更新的。


function subscribeUpdates () {
    
    
  ...

  const checkForUpdates = () => {
    
    
		...
    
    newChildProps = childPropsSelector(
      latestStoreState,
      lastWrapperProps.current
    )
    
    if (newChildProps === lastChildProps.current) {
    
    
      ...
    } else {
    
    
      ...
      
      forceComponentUpdateDispatch({
    
    
        type: 'STORE_UPDATED',
        payload: {
    
    
          error
        }
      })
      
      ...
    }
    
  }
  ...
}

上面的代码可以看出来如果store更新新计算出来的newChildProps和lastChildProps.current(上一次更新的childProps)一样的话就不会主动触发当前组件更新(调用方法forceComponentUpdateDispatch)。

所以现在组要看newChildProps的计算过程,也就是childPropsSelector(latestStoreState, lastWrapperProps.current)的调用,它是怎么在多次调用的时候返回的引用是相同的(对象是相同的可以通过===严格等于)。

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

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

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

这里面的东西有点多而且很巧妙,所以下一章继续解读。

猜你喜欢

转载自blog.csdn.net/letterTiger/article/details/107680992