「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」
前文分析了Provider的源码,以及用Context全局状态管理的问题,本文将进入connect的源码,看看react-redux是如何解决这些问题的。
connect
connect的核心代码在connectAdvanced.js中:
export default function connectAdvanced(...) {
...
return function wrapWithConnect(WrappedComponent){
...
function ConnectFunction(props) {
...
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
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)
}
}
这里有几个函数和变量:
- wrapWithConnect:
connect()返回的高阶组件。高阶组件是参数为组件,返回值为新组件的函数 - ConnectFunction:
connect()(WrappedComponent)返回的真正组件 - shouldHandleStateChanges:
connect可以传递4个参数:mapStateToProps,mapDispatchToProps,mergeProps,options。如果没有传递mapStateToProps,意味着这个组件对store中的state没有依赖,也就不需要处理state变更后的组件更新。而如果传递了这个参数,那么当state变更后,就需要触发这个组件的更新。 - ContextToUse:默认情况下它的值就是ReactReduxContext
- overriddenContextValue:默认情况下是下面这个对象:首先获取到Provider中创建的context value,然后创建了一个新的subscription
const contextValue = useContext(ContextToUse)
...
{
...contextValue,
subscription,
}
- hoistStatics: 这个函数是将WrappedComponent上的static方法传递到Connect组件上
可以connect将组件包在了Context的Provider中:每个被connect的组件都单独在一个Context.Provider中,虽然这个方法可以破解Context的全局更新,但是并不必要,这个设计有这另一个目的。
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
如果监听到store变化,在组件中调用类似forceUpdate的方法,也能做到局部的re-render。
如何触发更新
在ConnectFunction中有这样一段代码
function ConnectFunction(props) {
...
// 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]
)
...
}
它相当于
useLayoutEffect(()=>{
subscribeUpdates(
shouldHandleStateChanges,
store,
subscription,
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
childPropsFromStoreUpdate,
notifyNestedSubs,
forceComponentUpdateDispatch
)
},[store, subscription, childPropsSelector])
其中subscribeUpdates的执行是得组件订阅了store中state的变化:
function subscribeUpdates(...){
...
const checkForUpdates = () => {
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error,
},
})
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
...
}
subscription.trySubscribe是将subscription.onStateChange挂入redux store的listeners中,当相应到变化后使用forceComponentUpdateDispatch进行组件更新,这个函数是useReducer返回的一个dispatch,它每次调用都会改变useReducer中的state,从而触发了组件更新。
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
const [,forceComponentUpdateDispatch,] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
为什么设计多层嵌套的Context.Provider
这个设计得从一个api说起:useContext
useContext
const value = useContext(MyContext);
这个api返回的是最近的一个Context.Provider的value值
Accepts a context object (the value returned from
React.createContext) and returns the current context value for that context. The current context value is determined by thevalueprop of the nearest<MyContext.Provider>above the calling component in the tree.
结合createSubscription和connect中subscribeUpdates的源码:
// 这里可以传一个父级的subscription
function createSubscription(store, parentSub) {
function notifyNestedSubs() {
listeners.notify()
}
// 当传入了parentSub时,传入的listener是挂在parentSub,而不是store上的
function trySubscribe() {
if (!unsubscribe) {
unsubscribe = parentSub
? parentSub.addNestedSub(handleChangeWrapper)
: store.subscribe(handleChangeWrapper)
listeners = createListenerCollection()
}
}
...
}
const checkForUpdates = () => {
// If the child props haven't changed, nothing to do here - cascade the subscription update
if (newChildProps === lastChildProps.current) {
if (!renderIsScheduled.current) {
notifyNestedSubs()
}
}
}
当connect的组件中计算出的props没有变化,也即这个父组件本身不需要更新,那么就调用子组件的更新判断。
这个设计能够让我们避免一种重复更新的情况:
如果每个组件单独监听store的改变并强制re-render,那么当父组件更新时,子组件机会因为父组件的更新而更新,也会因为自身监听到state的改变而更新。
useSelector
在useSelector的源码中也有相似的触发更新的方法,
const [, forceRender] = useReducer((s) => s + 1, 0)
...
function checkForUpdates() {
...
forceRender()
}
但是useSelector中无法再包裹Context.Provider,因此如果这个组件的子组件也使用了useSelector,那么可能会出现父子组件同时被forceRender的情况,造成子组件被渲染两次。
图中Author是Paragraph的子组件,并且都用到了store中的颜色,当改变颜色是,触发了两次forceRender。
但重复渲染,因为react自身也有更新合并。如果要测试这个特性,可以将forceRender套在flushSync中
flushSync(()=>{ forceRender()})
总结
react-redux不仅基于Context将redux与react结合了起来,并且还进行了各种性能优化,其中最精彩的便是全局状态和局部更新的协调,因此不要被它源码中随处可见的useMemo、props比较,unstable_batchedUpdates一叶蔽目而不见森林。