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

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

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

總結(jié)
接下來(lái)我們總結(jié)一下整個(gè)connect的流程。我們還是從訂閱和更新兩個(gè)方向入手。
訂閱流程
整個(gè)訂閱的流程是,如果被connect包裹,并且具有第一個(gè)參數(shù)。首先通過(guò)context獲取最近的 subscription,然后創(chuàng)建一個(gè)新的subscription,并且和父級(jí)的subscription建立起關(guān)聯(lián)。當(dāng)?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里,進(jìn)行訂閱,將自己的訂閱函數(shù)checkForUpdates,作為回調(diào)函數(shù),通過(guò)trySubscribe 和this.parentSub.addNestedSub ,加入到父級(jí)subscription的listeners中。由此完成整個(gè)訂閱流程。
更新流程
整個(gè)更新流程是,那state改變,會(huì)觸發(fā)根訂閱器的store.subscribe,然后會(huì)觸發(fā)listeners.notify ,也就是checkForUpdates函數(shù),然后checkForUpdates函數(shù)首先根據(jù)mapStoretoprops,mergeprops等操作,驗(yàn)證該組件是否發(fā)起訂閱,props 是否改變,并更新,如果發(fā)生改變,那么觸發(fā)useReducer的forceComponentUpdateDispatch函數(shù),來(lái)更新業(yè)務(wù)組件,如果沒(méi)有發(fā)生更新,那么通過(guò)調(diào)用notifyNestedSubs,來(lái)通知當(dāng)前subscription的listeners檢查是否更新,然后盡心層層checkForUpdates,逐級(jí)向下,借此完成整個(gè)更新流程。
四 關(guān)于 useMemo 用法思考?
整個(gè)react-redux源碼中,對(duì)于useMemo用法還是蠻多的,我總結(jié)了幾條,奉上????:
1 緩存屬性 / 方法
react-redux源碼中,多處應(yīng)用了useMemo 依賴/緩存 屬性的情況。這樣做的好處是只有依賴項(xiàng)發(fā)生改變的時(shí)候,才更新新的緩存屬性/方法,比如 childPropsSelector , subscription , actualChildProps 等主要方法屬性。
2 控制組件渲染,渲染節(jié)流。
react-redux源碼中,通過(guò) useMemo來(lái)控制業(yè)務(wù)組件是否渲染。通過(guò) actualChildProps變化,來(lái)證明是否來(lái)自 **自身 props ** 或 訂閱的 state 的修改,來(lái)確定是否渲染組件。
例子??:
const renderedWrappedComponent = useMemo(() => (<WrappedComponent{...actualChildProps}ref={reactReduxForwardedRef}/>),[reactReduxForwardedRef, WrappedComponent, actualChildProps])
五 總結(jié)
希望這篇文章能讓屏幕前的你,對(duì)react-redux的訂閱和更新流程有一個(gè)新的認(rèn)識(shí)。送人玫瑰,手留余香,閱讀的朋友可以給筆者點(diǎn)贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-) 歡迎加我微信「TH0000666」一起交流學(xué)習(xí)... 關(guān)注公眾號(hào)「前端Sharing」,持續(xù)為你推送精選好文。
