阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
- 阅读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
阅读react-redux源码(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates相关推荐
- 阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理
阅读react-redux源码 - 零 阅读react-redux源码 - 一 阅读react-redux源码(二) - createConnect.match函数的实现 阅读react-redux源 ...
- 逐行阅读redux源码(二)combineReducers
前情提要 逐行阅读redux源码(一)createStore 认识reducers 在我们开始学习源码之前,我们不妨先来看看何谓reducers: 如图所见,我们可以明白, reducer 是用来对初 ...
- [react] 你阅读了几遍React的源码?都有哪些收获?你是怎么阅读的?
[react] 你阅读了几遍React的源码?都有哪些收获?你是怎么阅读的? 0遍 +1 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主目录 与歌 ...
- Redux 源码解读 —— 从源码开始学 Redux
已经快一年没有碰过 React 全家桶了,最近换了个项目组要用到 React 技术栈,所以最近又复习了一下:捡起旧知识的同时又有了一些新的收获,在这里作文以记之. 在阅读文章之前,最好已经知道如何使用 ...
- 学习 redux 源码整体架构,深入理解 redux 及其中间件原理
如果觉得内容不错,可以设为星标置顶我的公众号 1. 前言 你好,我是若川.这是学习源码整体架构系列第八篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是 ...
- 中秋福利 | 漂亮的React后台源码真情大放送
中秋快乐 每逢佳节倍思亲,一年一度的中秋,你和谁一起度过?如果你和小编一样,漂泊在外,别忘记给远在家乡的父母打个电话,祝福他们中秋快乐,告诉他们自己还好,勿让他们挂念.在此小编,祝各位粉丝们" ...
- redux源码分析之一:createStore.js
欢迎关注redux源码分析系列文章: redux源码分析之一:createStore.js redux源码分析之二:combineReducers.js redux源码分析之三:bindActionC ...
- 深入理解redux之从redux源码到react-redux的原理
在使用react的过程中,用redux来管理应用中的状态,使应用流更清晰的同时也会有小小的疑惑,比如reducer在redux中时怎么发挥作用的,为什么只要写好reducer,和dispatch特定a ...
- golangsha1解码_如何阅读Golang的源码?
Go 的源码在安装包的 src/ 目录下.怎么看它的源码呢?直接看吧!没人教的情况下,只能自己撸了.当然,这种内容一般也不会有人教. 怎么撸? Go 源码中,应该可分为与语言息息相关的部分,和官方提供 ...
最新文章
- Airflow安装部署
- 《oracle大型数据库系统在AIX/unix上的实战详解》讨论31: oracle、sybase 数据库的不同访问...
- boost::mpi模块all_to_all() 集合的测试
- 心跳检测以及应用层心跳包机制设计
- vux 混合式开发
- bat产品经理能力模型_浅析产品经理能力模型
- 总结better-scroll插件的使用
- SDOI2018 旧试题
- window 软件 C盘 文件 搬家(配置文件搬家)
- python验证软件签名
- 【电脑配置】开发人员必备,黑苹果双系统安装教程
- R语言之K-mean聚类分析
- 搭建一个网站步骤 制作网页完整步骤
- activity工作流引擎
- 用网页服务器实现钢琴弹奏(使用Wizwiki-W7500)
- RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat
- Physical CPU、Logical CPU、Core、Thread、Socket
- Facebook 疯狂投资 AR 技术!
- Clark与Park变换推导
- ups不间断电源高频机和工频机UPS不间断电源的比较
热门文章
- 打砖块小游戏php程序,利用原生js实现html5打砖块小游戏(代码示例)
- python中__init__.py的作用、module和package
- Python中什么是set
- [单选]物联网产业链的主要产品不包括下列哪一项 - 关于物联网(主讲:柳毅)笔记...
- docker 查看日志_8个优秀Docker容器监控工具,收藏了
- OpenCV copyMakeBorder()来拓展边界
- matlab 大括号
- 计算机基础知识:什么是位、字节、字、KB、MB
- UTF-8、GB2312、GB18030、GBK和BIG5等字符集编码范围的具体说明
- java 中map_Java中Map集合