- 阅读react-redux源码 - 零
- 阅读react-redux源码 - 一
- 阅读react-redux源码(二) - createConnect、match函数的实现
- 阅读react-redux源码(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
终于到了最核心的connectAdvanced.js文件,在这里做的最主要的事情就是响应Provider提供的store的改变。除了响应store的变化,还做了很多事情,分别是:ref的处理,是否是pure模式,还有对于store改变的事件的转发。
首先回顾下connectAdvanced函数在connect.js中是如何被使用的。
return connectHOC(selectorFactory, {// used in error messagesmethodName: '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 changesshouldHandleStateChanges: Boolean(mapStateToProps),// passed through to selectorFactoryinitMapStateToProps,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 changesshouldHandleStateChanges = true,// REMOVED: the key of props/context to get the storestoreKey = 'store',// REMOVED: expose the wrapped component via refswithRef = false,// use React's forwardRef to expose a ref of the wrapped componentforwardRef = false,// the context consumer to usecontext = 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 logicconst 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 changesuseIsomorphicLayoutEffectWithArgs(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] = statereturn [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 comparisonslastWrapperProps.current = wrapperPropslastChildProps.current = actualChildPropsrenderIsScheduled.current = false// If the render was from a store update, clear out that reference and cascade the subscriber updateif (childPropsFromStoreUpdate.current) {childPropsFromStoreUpdate.current = nullnotifyNestedSubs()}
}
这里做了两件事情,一个是缓存了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 hereif (!shouldHandleStateChanges) return// Capture values for checking if and when this component unmountslet didUnsubscribe = falselet lastThrownError = null// We'll run this callback every time a store subscription update propagates to this componentconst checkForUpdates = () => {if (didUnsubscribe) {// Don't run stale listeners.// Redux doesn't guarantee unsubscriptions happen until next dispatch.return}const latestStoreState = store.getState()let newChildProps, errortry {// Actually run the selector with the most recent store state and wrapper props// to determine what the child props should benewChildProps = childPropsSelector(latestStoreState,lastWrapperProps.current)} catch (e) {error = elastThrownError = e}if (!error) {lastThrownError = null}// If the child props haven't changed, nothing to do here - cascade the subscription updateif (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 = newChildPropschildPropsFromStoreUpdate.current = newChildPropsrenderIsScheduled.current = true// If the child props _did_ change (or we caught an error), this wrapper component needs to re-renderforceComponentUpdateDispatch({type: 'STORE_UPDATED',payload: {error}})}}// Actually subscribe to the nearest connected ancestor (or store)subscription.onStateChange = checkForUpdatessubscription.trySubscribe()// Pull data from the store after first render in case the store has// changed since we began.checkForUpdates()const unsubscribeWrapper = () => {didUnsubscribe = truesubscription.tryUnsubscribe()subscription.onStateChange = nullif (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 = trueforceComponentUpdateDispatch({type: 'STORE_UPDATED',payload: {error}
})
需要更新当前组件需要调用方法forceComponentUpdateDispatch
,并且设置缓存上lastChildProps.current = newChildProps
和childPropsFromStoreUpdate.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 = nullnotifyNestedSubs()}...}
更新结束后会一次性通知有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
,接下来将从生下的角度查看该函数做的事情。
- 阅读react-redux源码 - 零
- 阅读react-redux源码 - 一
- 阅读react-redux源码(二) - createConnect、match函数的实现
- 阅读react-redux源码(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates