「源碼解析」一文吃透react-redux源碼(useMemo經(jīng)典源碼級案例)
前言
使用過redux的同學都知道,redux作為react公共狀態(tài)管理工具,配合react-redux可以很好的管理數(shù)據(jù),派發(fā)更新,更新視圖渲染的作用,那么對于 react-redux 是如何做到根據(jù) state 的改變,而更新組件,促使視圖渲染的呢,讓我們一起來探討一下,react-redux 源碼的奧妙所在。
在正式分析之前我們不妨來想幾個問題:
1 為什么要在 root 根組件上使用 react-redux 的 Provider 組件包裹?
2 react-redux 是怎么和 redux 契合,做到 state 改變更新視圖的呢?
3 provide 用什么方式存放當前的 redux 的 store, 又是怎么傳遞給每一個需要管理state的組件的?
4 connect 是怎么樣連接我們的業(yè)務組件,然后傳遞我們組件更新函數(shù)的呢?
5 connect 是怎么通過第一個參數(shù),來訂閱與之對應的 state 的呢?
6 connect 怎么樣將 props,和 redux的 state 合并的?

帶著這些疑問我們不妨先看一下 Provider 究竟做了什么?
一 Provider 創(chuàng)建Subscription,context保存上下文
/* provider 組件代碼 */function Provider({ store, context, children }) {/* 利用useMemo,跟據(jù)store變化創(chuàng)建出一個contextValue 包含一個根元素訂閱器和當前store */const contextValue = useMemo(() => {/* 創(chuàng)建了一個根 Subscription 訂閱器 */const subscription = new Subscription(store)/* subscription 的 notifyNestedSubs 方法 ,賦值給 onStateChange方法 */subscription.onStateChange = subscription.notifyNestedSubsreturn {store,subscription} /* store 改變創(chuàng)建新的contextValue */}, [store])/* 獲取更新之前的state值 ,函數(shù)組件里面的上下文要優(yōu)先于組件更新渲染 */const previousState = useMemo(() => store.getState(), [store])useEffect(() => {const { subscription } = contextValue/* 觸發(fā)trySubscribe方法執(zhí)行,創(chuàng)建listens */subscription.trySubscribe() // 發(fā)起訂閱if (previousState !== store.getState()) {/* 組件更新渲染之后,如果此時state發(fā)生改變,那么立即觸發(fā) subscription.notifyNestedSubs 方法 */subscription.notifyNestedSubs()}/* */return () => {subscription.tryUnsubscribe() // 卸載訂閱subscription.onStateChange = null}/* contextValue state 改變出發(fā)新的 effect */}, [contextValue, previousState])const Context = context || ReactReduxContext/* context 存在用跟元素傳進來的context ,如果不存在 createContext創(chuàng)建一個context ,這里的ReactReduxContext就是由createContext創(chuàng)建出的context */return <Context.Provider value={contextValue}>{children}</Context.Provider>}
從源碼中provider作用大致是這樣的
1 首先創(chuàng)建一個 contextValue ,里面包含一個創(chuàng)建出來的父級 Subscription (我們姑且先稱之為根級訂閱器)和redux提供的store。
2 通過react上下文context把 contextValue 傳遞給子孫組件。
二 Subscription訂閱消息,發(fā)布更新
在我們分析了不是很長的 provider 源碼之后,隨之一個 Subscription 出現(xiàn),那么這個 Subscription 由什么作用呢??????,我們先來看看在 Provder 里出現(xiàn)的Subscription 方法。
notifyNestedSubs trySubscribe tryUnsubscribe
在整個 react-redux 執(zhí)行過程中 Subscription 作用非常重要,這里方便先透漏一下,他的作用是收集所有被 connect 包裹的組件的更新函數(shù) onstatechange,然后形成一個 callback 鏈表,再由父級 Subscription 統(tǒng)一派發(fā)執(zhí)行更新,我們暫且不關(guān)心它是怎么運作的,接下來就是 Subscription 源碼 ,我們重點看一下如上出現(xiàn)的三個方法。
/* 發(fā)布訂閱者模式 */export default class Subscription {constructor(store, parentSub) {this.store = storethis.parentSub = parentSubthis.unsubscribe = nullthis.listeners = nullListenersthis.handleChangeWrapper = this.handleChangeWrapper.bind(this)}/* 負責檢測是否該組件訂閱,然后添加訂閱者也就是listener */addNestedSub(listener) {this.trySubscribe()return this.listeners.subscribe(listener)}/* 向listeners發(fā)布通知 */notifyNestedSubs() {this.listeners.notify()}/* 對于 provide onStateChange 就是 notifyNestedSubs 方法,對于 connect 包裹接受更新的組件 ,onStateChange 就是 負責更新組件的函數(shù) 。*/handleChangeWrapper() {if (this.onStateChange) {this.onStateChange()}}/* 判斷有沒有開啟訂閱 */isSubscribed() {return Boolean(this.unsubscribe)}/* 開啟訂閱模式 首先判斷當前訂閱器有沒有父級訂閱器 , 如果有父級訂閱器(就是父級Subscription),把自己的handleChangeWrapper放入到監(jiān)聽者鏈表中 */trySubscribe() {/*parentSub 即是provide value 里面的 Subscription 這里可以理解為 父級元素的 Subscription*/if (!this.unsubscribe) {this.unsubscribe = this.parentSub? this.parentSub.addNestedSub(this.handleChangeWrapper)/* provider的Subscription是不存在parentSub,所以此時trySubscribe 就會調(diào)用 store.subscribe */: this.store.subscribe(this.handleChangeWrapper)this.listeners = createListenerCollection()}}/* 取消訂閱 */tryUnsubscribe() {if (this.unsubscribe) {this.unsubscribe()this.unsubscribe = nullthis.listeners.clear()this.listeners = nullListeners}}}
看完 Provider 和 Subscription源碼,我來解釋一下兩者到底有什么關(guān)聯(lián),首先Provider創(chuàng)建 Subscription 時候沒有第二個參數(shù),就說明provider 中的Subscription 不存在 parentSub 。那么再調(diào)用Provider組件中useEffect鉤子中trySubscribe的時候,會觸發(fā)this.store.subscribe , subscribe 就是 redux 的 subscribe ,此時真正發(fā)起了訂閱。
subscription.onStateChange = subscription.notifyNestedSubs有此可知,最終state改變,觸發(fā)的是notifyNestedSubs方法。我們再一次看看這個notifyNestedSubs。
/* 向listeners發(fā)布通知 */notifyNestedSubs() {this.listeners.notify()}
最終向當前Subscription 的訂閱者們發(fā)布 notify更新。
Subscription總結(jié) - 發(fā)布訂閱模式的實現(xiàn)
綜上所述我們總結(jié)一下。Subscription 的作用,首先通過 trySubscribe 發(fā)起訂閱模式,如果存在這父級訂閱者,就把自己更新函數(shù)handleChangeWrapper,傳遞給父級訂閱者,然后父級由 addNestedSub 方法將此時的回調(diào)函數(shù)(更新函數(shù))添加到當前的 listeners 中 。如果沒有父級元素(Provider的情況),則將此回調(diào)函數(shù)放在store.subscribe中,handleChangeWrapper 函數(shù)中onStateChange,就是 Provider 中 Subscription 的 notifyNestedSubs 方法,而 notifyNestedSubs 方法會通知listens 的 notify 方法來觸發(fā)更新。這里透漏一下,子代Subscription會把更新自身handleChangeWrapper傳遞給parentSub,來統(tǒng)一通知connect組件更新。
這里我們弄明白一個問題
react-redux 更新組件也是用了 store.subscribe 而且 store.subscribe 只用在了 Provider 的 Subscription中 (沒有 parentsub )
大致模型就是
state更改 -> store.subscribe -> 觸發(fā) provider 的 Subscription 的 handleChangeWrapper 也就是 notifyNestedSubs -> 通知 listeners.notify() -> 通知每個被 connect 容器組件的更新 -> callback 執(zhí)行 -> 觸發(fā)子組件Subscription 的 handleChangeWrapper ->觸發(fā)子 onstatechange(可以提前透漏一下,onstatechange保存了更新組件的函數(shù))。
前邊的內(nèi)容提到了**createListenerCollection,listeners**,但是他具體有什么作用我們接下來一起看一下。
function createListenerCollection() {/* batch 由getBatch得到的 unstable_batchedUpdates 方法 */const batch = getBatch()let first = nulllet last = nullreturn {/* 清除當前l(fā)isteners的所有l(wèi)istener */clear() {first = nulllast = null},/* 派發(fā)更新 */notify() {batch(() => {let listener = firstwhile (listener) {listener.callback()listener = listener.next}})},/* 獲取listeners的所有l(wèi)istener */get() {let listeners = []let listener = firstwhile (listener) {listeners.push(listener)listener = listener.next}return listeners},/* 接收訂閱,將當前的callback(handleChangeWrapper)存到當前的鏈表中 */subscribe(callback) {let isSubscribed = truelet listener = (last = {callback,next: null,prev: last})if (listener.prev) {listener.prev.next = listener} else {first = listener}/* 取消當前 handleChangeWrapper 的訂閱*/return function unsubscribe() {if (!isSubscribed || first === null) returnisSubscribed = falseif (listener.next) {listener.next.prev = listener.prev} else {last = listener.prev}if (listener.prev) {listener.prev.next = listener.next} else {first = listener.next}}}}}
batch
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'setBatch(batch)
我們可以得出結(jié)論 createListenerCollection 可以產(chǎn)生一個 listeners 。 listeners的作用。
1收集訂閱:以鏈表的形式收集對應的 listeners (每一個Subscription) 的handleChangeWrapper函數(shù)。
2派發(fā)更新:, 通過 batch 方法( react-dom 中的 unstable_batchedUpdates ) 來進行批量更新。
溫馨提示: React 的 unstable_batchedUpdate() API 允許將一次事件循環(huán)中的所有 React 更新都一起批量處理到一個渲染過程中。
總結(jié)
??到這里我們明白了:
1 react-redux 中的 provider 作用 ,通過 react 的 context 傳遞 subscription 和 redux 中的store ,并且建立了一個最頂部根 Subscription 。
2 Subscription 的作用:起到發(fā)布訂閱作用,一方面訂閱 connect 包裹組件的更新函數(shù),一方面通過 store.subscribe 統(tǒng)一派發(fā)更新。
3 Subscription 如果存在這父級的情況,會把自身的更新函數(shù),傳遞給父級 Subscription 來統(tǒng)一訂閱。
三 connect 究竟做了什么?
1 回顧 connect 用法
工慾善其事,必先利其器 ,想要吃透源碼之前,必須深度熟悉其用法。才能知其然知其所以然。我們先來看看高階組件connect用法。
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?mapStateToProps
const mapStateToProps = state => ({ todos: state.todos })作用很簡單,組件依賴redux的 state,映射到業(yè)務組件的 props中,state改變觸發(fā),業(yè)務組件props改變,觸發(fā)業(yè)務組件更新視圖。當這個參數(shù)沒有的時候,當前組件不會訂閱 store 的改變。
mapDispatchToProps
const mapDispatchToProps = dispatch => {return {increment: () => dispatch({ type: 'INCREMENT' }),decrement: () => dispatch({ type: 'DECREMENT' }),reset: () => dispatch({ type: 'RESET' })}}
將 redux 中的dispatch 方法,映射到,業(yè)務組件的props中。
mergeProps
/** stateProps , state 映射到 props 中的內(nèi)容* dispatchProps, dispatch 映射到 props 中的內(nèi)容。* ownProps 組件本身的 props*/(stateProps, dispatchProps, ownProps) => Object
正常情況下,如果沒有這個參數(shù),會按照如下方式進行合并,返回的對象可以是,我們自定義的合并規(guī)則。我們還可以附加一些屬性。
{ ...ownProps, ...stateProps, ...dispatchProps }
options
{context?: Object, // 自定義上下文pure?: boolean, // 默認為 true , 當為 true 的時候 ,除了 mapStateToProps 和 props ,其他輸入或者state 改變,均不會更新組件。areStatesEqual?: Function, // 當pure true , 比較引進store 中state值 是否和之前相等。(next: Object, prev: Object) => booleanareOwnPropsEqual?: Function, // 當pure true , 比較 props 值, 是否和之前相等。(next: Object, prev: Object) => booleanareStatePropsEqual?: Function, // 當pure true , 比較 mapStateToProps 后的值 是否和之前相等。(next: Object, prev: Object) => booleanareMergedPropsEqual?: Function, // 當 pure 為 true 時, 比較 經(jīng)過 mergeProps 合并后的值 , 是否與之前等 (next: Object, prev: Object) => booleanforwardRef?: boolean, //當為true 時候,可以通過ref 獲取被connect包裹的組件實例。}
options可以是如上屬性,上面已經(jīng)標注了每一個屬性的作用,這里就不多說了。
2 connect 初探
對于connect 組件 ,我們先看源碼一探究竟
/src/connect/connect.js
export function createConnect({connectHOC = connectAdvanced,mapStateToPropsFactories = defaultMapStateToPropsFactories,mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,mergePropsFactories = defaultMergePropsFactories,selectorFactory = defaultSelectorFactory} = {}) {return function connect(mapStateToProps,mapDispatchToProps,mergeProps,{pure = true,areStatesEqual = strictEqual,areOwnPropsEqual = shallowEqual,areStatePropsEqual = shallowEqual,areMergedPropsEqual = shallowEqual,...extraOptions} = {}) {/* 經(jīng)過代理包裝后的 mapStateToProps */const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories,'mapStateToProps' )/* 經(jīng)過代理包裝后的 mapDispatchToProps */const initMapDispatchToProps = match( mapDispatchToProps, mapDispatchToPropsFactories,'mapDispatchToProps')/* 經(jīng)過代理包裝后的 mergeProps */const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')return connectHOC(selectorFactory, {methodName: 'connect',getDisplayName: name => `Connect(${name})`,shouldHandleStateChanges: Boolean(mapStateToProps),initMapStateToProps,initMapDispatchToProps,initMergeProps,pure,areStatesEqual,areOwnPropsEqual,areStatePropsEqual,areMergedPropsEqual,...extraOptions})}}export default /*#__PURE__*/ createConnect()
我們先來分析一下整個函數(shù)做的事。
1 首先定一個 createConnect方法。傳入了幾個默認參數(shù),有兩個參數(shù)非常重要,connectHOC 作為整個 connect 的高階組件。selectorFactory 做為整合connect更新過程中的形成新props的主要函數(shù)。默認的模式是pure模式。
2 然后執(zhí)行createConnect方法,返回真正的connect函數(shù)本身。connect接收幾個參數(shù),然后和默認的函數(shù)進行整合,包裝,代理,最后形成三個真正的初始化函數(shù),這里的過程我們就先不講了。我們接下來分別介紹這三個函數(shù)的用途。
initMapStateToProps ,用于形成真正的 MapStateToProps函數(shù),將 store 中 state ,映射到 props
initMapDispatchToProps,用于形成真正的 MapDispatchToProps,將 dispatch 和 自定義的 dispatch 注入到props。
initMergeProps,用于形成真正的 mergeProps函數(shù),合并業(yè)務組件的 props , state 映射的 props , dispatch 映射的 props。
這里有一個函數(shù)非常重要,這個函數(shù)就是mergeProps, 請大家記住這個函數(shù),因為這個函數(shù)是判斷整個connect是否更新組件關(guān)鍵所在。上邊說過 connect基本用法的時候說過,當我們不向connect傳遞第三個參數(shù)mergeProps 的時候,默認的defaultMergeProps如下
/src/connect/mergeProps.js
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {return { ...ownProps, ...stateProps, ...dispatchProps }}
這個函數(shù)返回了一個新的對象,也就是新的props。而且將 業(yè)務組件 props , store 中的 state ,和 dispatch 結(jié)合到一起,形成一個新對象,作為新的 props 傳遞給了業(yè)務組件。
3 selectorFactory 形成新的props
前面說到selectorFactory 很重要,用于形成新的props,我們記下來看selectorFactory 源碼。
/src/connect/selectorFactory.js
export default function finalPropsSelectorFactory(dispatch,{ initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }) {// mapStateToProps mapDispatchToProps mergeProps 為真正connect 經(jīng)過一層代理的 proxy 函數(shù)const mapStateToProps = initMapStateToProps(dispatch, options)const mapDispatchToProps = initMapDispatchToProps(dispatch, options)const mergeProps = initMergeProps(dispatch, options)const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory// 返回一個 函數(shù)用于生成新的 propsreturn selectorFactory(mapStateToProps,mapDispatchToProps,mergeProps,dispatch,options)}
finalPropsSelectorFactory 的代碼很簡單, 首先得到真正connect 經(jīng)過一層代理函數(shù) mapStateToProps ,mapDispatchToProps ,mergeProps。然后調(diào)用selectorFactory (在pure模式下,selectorFactory 就是 pureFinalPropsSelectorFactory ) 。
可以這里反復用了閉包,可以剛開始有點蒙,不過靜下心來看發(fā)現(xiàn)其實不是很難。由于默認是pure,所以我們接下來主要看 pureFinalPropsSelectorFactory 函數(shù)做了些什么。
/** pure組件處理 , 對比 props 是否發(fā)生變化 然后 合并props */export function pureFinalPropsSelectorFactory(mapStateToProps,mapDispatchToProps,mergeProps,dispatch,{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual } //判斷 state prop 是否相等) {let hasRunAtLeastOnce = falselet statelet ownPropslet statePropslet dispatchPropslet mergedProps/* 第一次 直接形成 ownProps stateProps dispatchProps 合并 形成新的 props */function handleFirstCall(firstState, firstOwnProps) {state = firstStateownProps = firstOwnPropsstateProps = mapStateToProps(state, ownProps)dispatchProps = mapDispatchToProps(dispatch, ownProps)mergedProps = mergeProps(stateProps, dispatchProps, ownProps)hasRunAtLeastOnce = truereturn mergedProps}function handleNewPropsAndNewState() {// props 和 state 都改變 mergeProps}function handleNewProps() {// props 改變 mergeProps}function handleNewState() {// state 改變 mergeProps}/* 不是第一次的情況 props 或者 store.state 發(fā)生改變的情況。*/function handleSubsequentCalls(nextState, nextOwnProps) {/* 判斷兩次 props 是否相等 */const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)/* 判斷兩次 store.state 是否相等 */const stateChanged = !areStatesEqual(nextState, state)state = nextStateownProps = nextOwnPropsif (propsChanged && stateChanged) return handleNewPropsAndNewState()if (propsChanged) return handleNewProps()if (stateChanged) return handleNewState()return mergedProps}return function pureFinalPropsSelector(nextState, nextOwnProps) {return hasRunAtLeastOnce? handleSubsequentCalls(nextState, nextOwnProps): handleFirstCall(nextState, nextOwnProps)}}
這個函數(shù)處理邏輯很清晰。大致上做了這些事。通過閉包的形式返回一個函數(shù)pureFinalPropsSelector。pureFinalPropsSelector通過判斷是否是第一次初始化組件。
如果是第一次,那么直接調(diào)用mergeProps合并ownProps,stateProps,dispatchProps 形成最終的props。如果不是第一次,那么判斷到底是props還是 store.state 發(fā)生改變,然后針對那里變化,重新生成對應的props,最終合并到真正的props。
整個 selectorFactory 邏輯就是形成新的props傳遞給我們的業(yè)務組件。
4 connectAdvanced 形成真正包裹業(yè)務組件的 Hoc
接下來我們看一下 connect 返回的 connectAdvanced()到底做了什么,為了方便大家理解connect,我們這里先看看 connect 用法。
正常模式下:
const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })function Index(){/* ..... */return <div> { /* .... */ } </div>}export default connect(mapStateToProp)(Index)
裝飾器模式下:
const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })@connect(mapStateToProp)class Index extends React.Component{/* .... */render(){return <div> { /* .... */ } </div>}}
我們上面講到,connect執(zhí)行 接受 mapStateToProp 等參數(shù),最后返回 connectAdvanced() ,那么上述例子中connect執(zhí)行第一步connect(mapStateToProp)===connectAdvanced(),也就是connectAdvanced()執(zhí)行返回真正的hoc,用于包裹我們的業(yè)務組件。
接下來我們看 connectAdvanced 代碼
/src/components/connectAdvanced.js
export default function connectAdvanced(selectorFactory, // 每次 props,state改變執(zhí)行 ,用于生成新的 props。{getDisplayName = name => `ConnectAdvanced(${name})`,//可能被包裝函數(shù)(如connect())重寫methodName = 'connectAdvanced',//如果定義了,則傳遞給包裝元素的屬性的名稱,指示要呈現(xiàn)的調(diào)用。用于監(jiān)視react devtools中不必要的重新渲染。renderCountProp = undefined,shouldHandleStateChanges = true, //確定此HOC是否訂閱存儲更改storeKey = 'store',withRef = false,forwardRef = false, // 是否 用 forwarRef 模式context = ReactReduxContext,// Provider 保存的上下文...connectOptions} = {}) {/* ReactReduxContext 就是store存在的context */const Context = context/* WrappedComponent 為connect 包裹的組件本身 */return function wrapWithConnect(WrappedComponent){// WrappedComponent 被 connect 的業(yè)務組件本身}}
connectAdvanced接受配置參數(shù) , 然后返回真正的 HOC wrapWithConnect。
// 我們可以講下面的表達式分解connect(mapStateToProp)(Index)// 執(zhí)行 connectconnect(mapStateToProp)//返回connectAdvanced()//返回HOCwrapWithConnect
接下來我們分析一下wrapWithConnect到底做了些什么?
5 wrapWithConnect 高階組件
接下來我們來一起研究一下 wrapWithConnect,我們重點看一下 wrapWithConnect作為高階組件,會返回一個組件,這個組件會對原有的業(yè)務組件,進行一系列增強等工作。
function wrapWithConnect(WrappedComponent) {const wrappedComponentName =WrappedComponent.displayName || WrappedComponent.name || 'Component'const displayName = getDisplayName(wrappedComponentName)const selectorFactoryOptions = {...connectOptions,getDisplayName,methodName,renderCountProp,shouldHandleStateChanges,storeKey,displayName,wrappedComponentName,WrappedComponent}const { pure } = connectOptionsfunction createChildSelector(store) {// 合并函數(shù) mergeprops 得到最新的propsreturn selectorFactory(store.dispatch, selectorFactoryOptions)}//判斷是否是pure純組件模式 如果是 將用 useMemo 提升性能const usePureOnlyMemo = pure ? useMemo : callback => callback()// 負責更新的容器子組件function ConnectFunction (props){// props 為 業(yè)務組件 真正的 props}const Connect = pure ? React.memo(ConnectFunction) : ConnectFunctionConnect.WrappedComponent = WrappedComponentConnect.displayName = displayName/* forwardRef */if (forwardRef) {const forwarded = React.forwardRef(function forwardConnectRef(props,ref) {return <Connect {...props} reactReduxForwardedRef={ref} />})forwarded.displayName = displayNameforwarded.WrappedComponent = WrappedComponentreturn hoistStatics(forwarded, WrappedComponent)}return hoistStatics(Connect, WrappedComponent)}}
wrapWithConnect 的做的事大致分為一下幾點:
第一步
1 聲明負責更新的 ConnectFunction 無狀態(tài)組件。和負責合并 props 的createChildSelector方法
第二步
2 判斷是否是 pure 純組件模式,如果是用react.memo包裹,這樣做的好處是,會向 pureComponent 一樣對 props 進行淺比較。
第三步
3 如果 connect 有forwardRef配置項,用React.forwardRef處理,這樣做好處如下。
正常情況下因為我們的WrappedComponent 被 connect 包裝,所以不能通過ref訪問到業(yè)務組件WrappedComponent的實例。
子組件
const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })class Child extends React.Component{render(){/* ... */}}export default connect(mapStateToProp)(Child)
父組件
class Father extends React.Compoent{child = nullrender(){return <Child ref={(cur)=> this.child = cur } { /* 獲取到的不是`Child`本身 */ } />}}
我們無法通過 ref 訪問到 Child 組件。
所以我們可以通過 options 的 forwardRef 屬性設置為 true,這樣就可以根本解決問題。
connect(mapStateToProp,mapDispatchToProps,mergeProps,{ forwardRef:true })(Child)
第四步
hoistStatics(Connect, WrappedComponent)
最后做的事情就是通過hoistStatics庫 把子組件WrappedComponent的靜態(tài)方法/屬性,繼承到父組件Connect上。因為在 高階組件 包裝 業(yè)務組件的過程中,如果不對靜態(tài)屬性或是方法加以額外處理,是不會被包裝后的組件訪問到的,所以需要類似hoistStatics這樣的庫,來做處理。
接下來講的就是整個 connect的核心了。我們來看一下負責更新的容器ConnectFunction 到底做了些什么?
6 ConnectFunction 控制更新
ConnectFunction 的代碼很復雜,需要我們一步步去吃透,一步步去消化。
function ConnectFunction(props) {/* TODO: 第一步 把 context ForwardedRef props 取出來 */const [reactReduxForwardedRef,wrapperProps // props 傳遞的props] = useMemo(() => {const { reactReduxForwardedRef, ...wrapperProps } = propsreturn [reactReduxForwardedRef, wrapperProps]}, [props])// 獲取 context內(nèi)容 里面含有 redux 中store 和 subscriptionconst contextValue = useContext(Context)//TODO: 判斷 store 是否來此 props didStoreComeFromProps ,正常情況下 ,prop 中是不存在 store 所以 didStoreComeFromProps = falseconst didStoreComeFromProps =Boolean(props.store) &&Boolean(props.store.getState) &&Boolean(props.store.dispatch)const didStoreComeFromContext =Boolean(contextValue) && Boolean(contextValue.store)// 獲取 redux 中 storeconst store = didStoreComeFromProps ? props.store : contextValue.store// 返回merge函數(shù) 用于生成真正傳給子組件 propsconst childPropsSelector = useMemo(() => {return createChildSelector(store)}, [store])// TODO: 第二步 subscription 監(jiān)聽者實例const [subscription, notifyNestedSubs] = useMemo(() => {// 如果沒有訂閱更新,那么直接返回。if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAYconst subscription = new Subscription(store,didStoreComeFromProps ? null : contextValue.subscription // 和 上級 `subscription` 建立起關(guān)系。this.parentSub = contextValue.subscription)// notifyNestedSubs 觸發(fā) noticy 所有子代 listener 監(jiān)聽者 -> 觸發(fā)batch方法,觸發(fā) batchupdate方法 ,批量更新const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription)return [subscription, notifyNestedSubs]}, [store, didStoreComeFromProps, contextValue])/* 創(chuàng)建出一個新的contextValue ,把父級的 subscription 換成自己的 subscription */const overriddenContextValue = useMemo(() => {if (didStoreComeFromProps) {return contextValue}return {...contextValue,subscription}}, [didStoreComeFromProps, contextValue, subscription])const [[previousStateUpdateResult],forceComponentUpdateDispatch /* */] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)// TODO: 第三步const lastChildProps = useRef() //保存上一次 合并過的 props信息(經(jīng)過 ownprops ,stateProps , dispatchProps 合并過的 )const lastWrapperProps = useRef(wrapperProps) // 保存本次上下文執(zhí)行 業(yè)務組件的 propsconst childPropsFromStoreUpdate = useRef()const renderIsScheduled = useRef(false) // 當前組件是否處于渲染階段// actualChildProps 為當前真正處理過后,經(jīng)過合并的 propsconst actualChildProps = usePureOnlyMemo(() => {// 調(diào)用 mergeProps 進行合并,返回合并后的最新 porpsreturn childPropsSelector(store.getState(), wrapperProps)}, [store, previousStateUpdateResult, wrapperProps])/* 負責更新緩存變量,方便下一次更新的時候比較 */useEffect(()=>{captureWrapperProps(...[lastWrapperProps,lastChildProps,renderIsScheduled,wrapperProps,actualChildProps,childPropsFromStoreUpdate,notifyNestedSubs])})useEffect(()=>{subscribeUpdates(...[shouldHandleStateChanges,store,subscription,childPropsSelector,lastWrapperProps,lastChildProps,renderIsScheduled,childPropsFromStoreUpdate,notifyNestedSubs,forceComponentUpdateDispatch])},[store, subscription, childPropsSelector])// TODO: 第四步:reactReduxForwardedRef 是處理父級元素是否含有 forwardRef 的情況 這里可以忽略。const renderedWrappedComponent = useMemo(() => (<WrappedComponent{...actualChildProps}ref={reactReduxForwardedRef}/>),[reactReduxForwardedRef, WrappedComponent, actualChildProps])const renderedChild = useMemo(() => {//shouldHandleStateChanges 來源 connect是否有第一個參數(shù)if (shouldHandleStateChanges) {return (// ContextToUse 傳遞 context<ContextToUse.Provider value={overriddenContextValue}>{renderedWrappedComponent}</ContextToUse.Provider>)}return renderedWrappedComponent}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])return renderedChild}
為了方便大家更直觀的理解,我這里保留了影響流程的核心代碼,我會一步步分析 整個核心部分。想要弄明白這里,需要對 react-hooks 和 provider 有一些了解。
第一步
通過 props 分離出 reactReduxForwardedRef , wrapperProps 。reactReduxForwardedRef 是當開啟 ForwardedRef 模式下,父級傳過來的 React.forwaedRef。
然后判斷通過常量didStoreComeFromProps儲存當前,redux.store 是否來自 props, 正常情況下,我們的 store 都來自 provider ,不會來自props,所以我們可以把didStoreComeFromProps = true 。接下來我們獲取到 store,通過 store 來判斷是否更新真正的合并props函數(shù)childPropsSelector。
第二步 創(chuàng)建 子代 subscription, 層層傳遞新的 context(很重要)
這一步非常重要,判斷通過shouldHandleStateChanges判斷此 HOC 是否訂閱存儲更改,如果已經(jīng)訂閱了更新(此時connect 具有第一個參數(shù)),那么創(chuàng)建一個 subscription ,并且和上一層provider的subscription建立起關(guān)聯(lián)。this.parentSub = contextValue.subscription。然后分離出 subscription 和 notifyNestedSubs(notifyNestedSubs的作用是通知當前subscription的 listeners 進行更新的方法。) 。
然后通過 useMemo 創(chuàng)建出一個新的 contextValue ,把父級的 subscription 換成自己的 subscription。用于通過 Provider 傳遞新的 context。這里簡單介紹一下,運用了 Provider 可以和多個消費組件有對應關(guān)系。多個 Provider 也可以嵌套使用,里層的會覆蓋外層的數(shù)據(jù)。react-redux用context更傾向于Provider良好的傳遞上下文的能力。
接下來通過useReducer制造出真正觸發(fā)更新的forceComponentUpdateDispatch 函數(shù)。也就是整個 state 或者是 props改變,觸發(fā)組件更新的函數(shù)。為什么這么做呢?
筆者認為react-redxx這樣設計原因是希望connect自己控制自己的更新,并且多個上下級 connect不收到影響。所以一方面通過useMemo來限制業(yè)務組件不必要的更新,另一方面來通過forceComponentUpdateDispatch來更新 HOC 函數(shù),產(chǎn)生actualChildProps,actualChildProps 改變 ,useMemo執(zhí)行,觸發(fā)組件渲染。
第三步:保存信息,執(zhí)行副作用鉤子(最重要的部分到了)
這一步十分重要,為什么這么說呢,首先先通過useRef緩存幾個變量:
lastChildProps -> 保存上一次 合并過的 props 信息(經(jīng)過 ownprops ,stateProps , dispatchProps 合并過的 )。 lastWrapperProps -> 保存本次上下文執(zhí)行 業(yè)務組件的 props 。 renderIsScheduled -> 當前組件是否處于渲染階段。 actualChildProps -> actualChildProps 為當前真正處理過后,經(jīng)過合并的 props, 組件通過 dep -> actualChildProps,來判斷是否進行更新。
接下來執(zhí)行兩次 useEffect , 源碼中不是這個樣子的,我這里經(jīng)過簡化,第一個 useEffect 執(zhí)行了 captureWrapperProps ,captureWrapperProps 是干什么的呢?
//獲取包裝的propsfunction captureWrapperProps(lastWrapperProps,lastChildProps,renderIsScheduled,wrapperProps,actualChildProps,childPropsFromStoreUpdate,notifyNestedSubs) {lastWrapperProps.current = wrapperProps //子propslastChildProps.current = actualChildProps //經(jīng)過 megeprops 之后形成的 proprenderIsScheduled.current = false // 當前組件渲染完成}
captureWrapperProps 的作用很簡單,在一次組件渲染更新后,將上一次 合并前 和 合并后 的props,保存起來。這么做目的是,能過在兩次hoc執(zhí)行渲染中,對比props stateProps是否發(fā)生變化。從而確定是否更新 hoc,進一步更新組件。
執(zhí)行第二個 useEffect 是很關(guān)鍵。執(zhí)行subscribeUpdates 函數(shù),subscribeUpdates 是訂閱更新的主要函數(shù),我們一起來看看:
function subscribeUpdates(shouldHandleStateChanges,store,subscription,childPropsSelector,lastWrapperProps, //子propslastChildProps, //經(jīng)過 megeprops 之后形成的 proprenderIsScheduled,childPropsFromStoreUpdate,notifyNestedSubs,forceComponentUpdateDispatch) {if (!shouldHandleStateChanges) return// 捕獲值以檢查此組件是否卸載以及何時卸載let didUnsubscribe = falselet lastThrownError = null//store更新訂閱傳播到此組件時,運行此回調(diào)const checkForUpdates = ()=>{//....}subscription.onStateChange = checkForUpdates//開啟訂閱者 ,當前是被connect 包轉(zhuǎn)的情況 會把 當前的 checkForceUpdate 放在存入 父元素的addNestedSub中。subscription.trySubscribe()//在第一次呈現(xiàn)之后從存儲中提取數(shù)據(jù),以防存儲從我們開始就改變了。checkForUpdates()/* 卸載訂閱起 */const unsubscribeWrapper = () => {didUnsubscribe = truesubscription.tryUnsubscribe()subscription.onStateChange = null}return unsubscribeWrapper}
這絕對是整個訂閱更新的核心,首先聲明 store 更新訂閱傳播到此組件時的回調(diào)函數(shù)checkForUpdates把它賦值給onStateChange,如果store中的state發(fā)生改變,那么在組件訂閱了state內(nèi)容之后,相關(guān)聯(lián)的state改變就會觸發(fā)當前組件的onStateChange,來合并得到新的props,從而觸發(fā)組件更新。
然后subscription.trySubscribe()把訂閱函數(shù)onStateChange綁定給父級subscription,進行了層層訂閱。
最后,為了防止渲染后,store內(nèi)容已經(jīng)改變,所以首先執(zhí)行了一次checkForUpdates。那么checkForUpdates的作用很明確了,就是檢查是否派發(fā)當前組件的更新。
到這里我們明白了,react-redux 通過 subscription 進行層層訂閱。對于一層層的組件結(jié)構(gòu),整體模型圖如下:。

接下來我們看一下checkForUpdates
//store更新訂閱傳播到此組件時,運行此回調(diào)const checkForUpdates = () => {if (didUnsubscribe) {//如果寫在了return}// 獲取 store 里stateconst latestStoreState = store.getState()qlet newChildProps, errortry {/* 得到最新的 props */newChildProps = childPropsSelector(latestStoreState,lastWrapperProps.current)}//如果新的合并的 props沒有更改,則此處不做任何操作-層疊訂閱更新if (newChildProps === lastChildProps.current) {if (!renderIsScheduled.current) {notifyNestedSubs() /* 通知子代 subscription 觸發(fā) checkForUpdates 來檢查是否需要更新。*/}} else {lastChildProps.current = newChildPropschildPropsFromStoreUpdate.current = newChildPropsrenderIsScheduled.current = true// 此情況 可能考慮到 代碼運行到這里 又發(fā)生了 props 更新 所以觸發(fā)一個 reducer 來促使組件更新。forceComponentUpdateDispatch({type: 'STORE_UPDATED',payload: {error}})}}
checkForUpdates 通過調(diào)用 childPropsSelector來形成新的props,然后判斷之前的 prop 和當前新的 prop 是否相等。如果相等,證明沒有發(fā)生變化,無須更新當前組件,那么通過調(diào)用notifyNestedSubs來通知子代容器組件,檢查是否需要更新。如果不相等證明訂閱的store.state發(fā)生變化,那么立即執(zhí)行forceComponentUpdateDispatch來觸發(fā)組件的更新。
對于層層訂閱的結(jié)構(gòu),整個更新模型圖如下:

總結(jié)
接下來我們總結(jié)一下整個connect的流程。我們還是從訂閱和更新兩個方向入手。
訂閱流程
整個訂閱的流程是,如果被connect包裹,并且具有第一個參數(shù)。首先通過context獲取最近的 subscription,然后創(chuàng)建一個新的subscription,并且和父級的subscription建立起關(guān)聯(lián)。當?shù)谝淮?code style="box-sizing: border-box;font-family: Menlo, Monaco, Consolas, "Courier New", monospace;font-size: 0.87em;word-break: break-word;border-radius: 2px;overflow-x: auto;background-color: rgb(255, 245, 245);color: rgb(255, 80, 44);padding: 0.065em 0.4em;">hoc容器組件掛在完成后,在useEffect里,進行訂閱,將自己的訂閱函數(shù)checkForUpdates,作為回調(diào)函數(shù),通過trySubscribe 和this.parentSub.addNestedSub ,加入到父級subscription的listeners中。由此完成整個訂閱流程。
更新流程
整個更新流程是,那state改變,會觸發(fā)根訂閱器的store.subscribe,然后會觸發(fā)listeners.notify ,也就是checkForUpdates函數(shù),然后checkForUpdates函數(shù)首先根據(jù)mapStoretoprops,mergeprops等操作,驗證該組件是否發(fā)起訂閱,props 是否改變,并更新,如果發(fā)生改變,那么觸發(fā)useReducer的forceComponentUpdateDispatch函數(shù),來更新業(yè)務組件,如果沒有發(fā)生更新,那么通過調(diào)用notifyNestedSubs,來通知當前subscription的listeners檢查是否更新,然后盡心層層checkForUpdates,逐級向下,借此完成整個更新流程。
四 關(guān)于 useMemo 用法思考?
整個react-redux源碼中,對于useMemo用法還是蠻多的,我總結(jié)了幾條,奉上????:
1 緩存屬性 / 方法
react-redux源碼中,多處應用了useMemo 依賴/緩存 屬性的情況。這樣做的好處是只有依賴項發(fā)生改變的時候,才更新新的緩存屬性/方法,比如 childPropsSelector , subscription , actualChildProps 等主要方法屬性。
2 控制組件渲染,渲染節(jié)流。
react-redux源碼中,通過 useMemo來控制業(yè)務組件是否渲染。通過 actualChildProps變化,來證明是否來自 **自身 props ** 或 訂閱的 state 的修改,來確定是否渲染組件。
例子??:
const renderedWrappedComponent = useMemo(() => (<WrappedComponent{...actualChildProps}ref={reactReduxForwardedRef}/>),[reactReduxForwardedRef, WrappedComponent, actualChildProps])
五 總結(jié)
希望這篇文章能讓屏幕前的你,對react-redux的訂閱和更新流程有一個新的認識。送人玫瑰,手留余香,閱讀的朋友可以給筆者點贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。
