- 阅读react-redux源码 - 零
- 阅读react-redux源码 - 一
- 阅读react-redux源码(二) - createConnect、match函数的实现
- 阅读react-redux源码(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
- 阅读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 } = propsreturn [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 availableconst 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 trueconst store = didStoreComeFromProps ? props.store : contextValue.storeconst [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 donereturn {...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 } = propsreturn [forwardedRef, wrapperProps]}, [props])return <WrappedComponent ref={forwardedRef} />}return ConnectFunctionif (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])
这里面的东西有点多而且很巧妙,所以下一章继续解读。
- 阅读react-redux源码 - 零
- 阅读react-redux源码 - 一
- 阅读react-redux源码(二) - createConnect、match函数的实现
- 阅读react-redux源码(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
- 阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理