- 阅读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 } = 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])
这里面的东西有点多而且很巧妙,所以下一章继续解读。
- 阅读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模式的处理