[原始碼閱讀]高效能和可擴充套件的React-Redux
注意:文章很長,只想瞭解邏輯而不深入的,可以直接跳到。
初識
首先,從它暴露對外的 API
開始
ReactReduxContext /* 提供了 React.createContext(null) */ Provider /* 一個儲存資料的元件,渲染了ContextProvider,內部呼叫redux中store.subscribe 訂閱資料,每當redux中的資料變動,比較新值與舊值,判斷是否重新渲染 */ connect /* 一個高階元件,第一階傳入對資料處理方法,第二階傳入要渲染的元件 內部處理了: 1. 對引數的檢查 2. 對傳入的資料處理方法進行處理 (沒傳怎麼處理,傳了提供什麼引數,傳的型別不同怎麼處理,結果如何比較等等) 3. 靜態方法轉移 4. 對渲染元件的傳遞(傳遞給connectAdvanced) */ connectAdvanced /* 儲存每一次執行的資料,執行connect定義的方案和邏輯,新舊資料對比(全等對比),渲染元件 這裡作為公開API,如果我們去使用,那麼connect裡面的邏輯就需要我們自定義了。 */
現在對它的大概工作範圍有了解後,我們可以開始沿著執行順序分析。
抽絲
Provider
我們使用時,當寫完了redux的 reducer
, action
, bindActionCreators
, combineReducers
, createStore
這一系列內容後,
我們得到了一個 store
會先使用 <Provider store={store}
包裹住根元件。
這時, Provider
元件開始工作
componentDidMount() { this._isMounted = true this.subscribe() }
第一次載入,需要執行 subscribe
subscribe
是什麼呢,就是對 redux
的 store
執行 subscribe
一個自定義函式,
這樣,每當資料變動,這個函式便會執行
subscribe() { const { store } = this.props // redux 的 store 訂閱 // 訂閱後,每當state改變 則自動執行這個函式 this.unsubscribe = store.subscribe(() => { // store.getState() 獲取最新的 state const newStoreState = store.getState() // 元件未載入,取消 if (!this._isMounted) { return } // 比較state是否相等,全等的不更新 this.setState(providerState => { if (providerState.storeState === newStoreState) { return null } return { storeState: newStoreState } }) }) /* ... */ }
看到嗎,這個自定義函式非常簡單,每次收到資料,進行全等比較,不等則更新資料。
這個元件的另2個生命週期函式:
componentWillUnmount() { if (this.unsubscribe) this.unsubscribe() this._isMounted = false } componentDidUpdate(prevProps) { // 比較store是否相等,如果相等則跳過 if (this.props.store !== prevProps.store) { // 取消訂閱之前的,再訂閱現在的(因為資料(store)不同了) if (this.unsubscribe) this.unsubscribe() this.subscribe() } }
這2段的意思就是,每當資料變了,就取消上一次資料的訂閱,在訂閱本次的資料,
當要銷燬元件,取消訂閱。
一段題外話(可跳過):
這個邏輯用 Hooks
的 useEffect
簡直完美匹配!
useEffect(()=>{ subscribe() return ()=>{ unSubscribe() } },props.data)
這段的意思就是,當 props.data
發生改變,執行 unSubscribe()
,再執行 subscribe()
。
邏輯完全一致有沒有!
最後的 render
:
這裡 Context
就是 React.createContext(null)
<Context.Provider value={this.state}> {this.props.children} </Context.Provider>
到這裡我稱為 react-redux
的第一階段。
一個小總結,第一階段就做了1件事:
定義了 Provider
元件,內部訂閱了 store
。
connect
到主菜了,先看它的 export
export default createConnect()
一看,我們應該有個猜測,這貨 createConnect
是個高階函式。
看看它的引數吧。
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) { /* ... */ }
題外話:一個編寫預設物件內部含有預設值的方法
function a({x=1,y=2}={}){} a()// x:1,y:2 a({})// x:1,y:2 a({x:2,z:5}) //x:2,y:2
這裡先說明一下它的引數,後面讀起來會很順。
connectHOC: 一個重要元件,用於執行已確定的邏輯,渲染最終元件,後面會詳細說。 mapStateToPropsFactories: 對 mapStateToProps 這個傳入的引數的型別選擇一個合適的方法。 mapDispatchToPropsFactories: 對 mapDispatchToProps 這個傳入的引數的型別選擇一個合適的方法。 mergePropsFactories: 對 mergeProps 這個傳入的引數的型別選擇一個合適的方法。 selectorFactory: 以上3個只是簡單的返回另一個合適的處理方法,它則執行這些處理方法,並且對結果定義瞭如何比較的邏輯。
可能有點繞,但 react-redux
就是這麼一個個高階函式組成的, selectorFactory
後面會詳細說。
首先我們再次確定這3個名字很長,實際很簡單的函式(原始碼這裡不放了)
mapStateToPropsFactories
mapDispatchToPropsFactories
mergePropsFactories
它們只是判斷了引數是否存在,是什麼型別,並且返回一個合適的處理方法,它們並沒有任何處理邏輯。
-
舉個例子:
const MyComponent=connect((state)=>state.articles})
這裡我只定義了
mapStateToProps
,並且是個function
,那麼mapStateToPropsFactories
就會返回一個處理
function
的方法。我沒有定義
mapDispatchToProps
,那麼mapDispatchToPropsFactories
檢測不到引數,則會提供一個預設值
dispatch => ({ dispatch })
,返回一個處理非function
(object)的方法。
那麼處理邏輯是誰定義呢?
wrapMapToProps
wrapMapToProps.js
這個檔案內部做了以下事情:
- 定義了一個處理
object
的方法(簡單的返回即可,因為最終目的就是要object)。 - 定義了一個處理
函式
和高階函式
(執行2次)的方法,這個方法比上面的複雜在於它需要檢測引數是否訂閱了ownProps
。
檢測方法很簡單,就是檢查引數的 length
(這裡 dependsOnOwnProps
是上一次檢查的結果,如果存在則不需要再次檢查)
export function getDependsOnOwnProps(mapToProps) { return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined ? Boolean(mapToProps.dependsOnOwnProps) : mapToProps.length !== 1 }
回到connect,繼續往下看
export function createConnect({ /* 上面所講的引數 */ } = {}) { return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) { /* ... */ } }
已經到了我們傳遞引數的地方,前3個引數意思就不解釋了,最後的引數 options
areStatesEqual = strictEqual,// ===比較 areOwnPropsEqual = shallowEqual,// 淺比較 areStatePropsEqual = shallowEqual,// 淺比較 areMergedPropsEqual = shallowEqual,// 淺比較
它們用在 selectorFactory
這個比較資料結果的方法內部。
繼續往下看
export function createConnect({ /* 上面已講 */ } = {}) { return function connect( /* 上面已講 */ ) { const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories, 'mapStateToProps' ) const initMapDispatchToProps = match( mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps' ) const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
這裡定義了3個變數(函式), match
的作用是什麼?
以 mapStateToProps
舉例來說,
因為上面也說了, mapStateToPropsFactories
裡面有多個方法,需要找到一個適合 mapStateToProps
的,
match
就是幹這事了。
match
方法內部遍歷 mapStateToPropsFactories
所有的處理方法,任何一個方法能夠匹配引數 mapStateToProps
,便被 match
捕獲返回,
如果一個都找不到則報錯提示引數配置錯誤。
現在這3個變數定義明確了,都是對應的引數的合適的處理方法。
至此,我們已經完成了第二階段,
做個小總結,第二階段做了哪些事:
-
connect
接收了對引數處理方案(3個...Factories
)。 -
connect
接收了引數的結果比較方案(selectFactory
) -
connect
接收了引數(mapStateToProps
,mapDispatchToProps
,mergeProps
,options
)。 - 定義了比較方案(4個
are...Equal
,其實就是全等比較
和淺比較
)。
前2個階段都是定義階段,接下來需要我們傳入自定義元件,也就是最後一個階段
connect(...)(Component)
接著看 connect
原始碼
export function createConnect({ /* 上面已講 */ } = {}) { return function connect( /* 上面已講 */ ) { /* 上面已講 */ return connectHOC(selectorFactory, { // 方法名稱,用在錯誤提示資訊 methodName: 'connect', // 最終渲染的元件名稱 getDisplayName: name => `Connect(${name})`, shouldHandleStateChanges: Boolean(mapStateToProps), // 以下是傳遞給 selectFactory initMapStateToProps, initMapDispatchToProps, initMergeProps, pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, // any extra options args can override defaults of connect or connectAdvanced ...extraOptions }) } }
這裡執行了 connectHOC()
,傳遞了上面已經講過的引數,而 connectHOC = connectAdvanced
因此我們進入最後一個對外 API
, connectAdvanced
connectAdvanced
connectAdvanced
函式,之前也提過,就是一個執行、元件渲染和元件更新的地方。
它裡面沒有什麼新概念,都是將我們上面講到的引數進行呼叫,最後根據結果進行渲染新元件。
還是從原始碼開始
export default function connectAdvanced( selectorFactory, { // 執行後作用於connect這個HOC元件名稱 getDisplayName = name => `ConnectAdvanced(${name})`, // 用於錯誤提示 methodName = 'connectAdvanced', // 有REMOVED標誌,這裡不關注 renderCountProp = undefined, // 確定connect這個HOC是否訂閱state變動,好像已經沒有用到了 shouldHandleStateChanges = true, // 有REMOVED標誌,這裡不關注 storeKey = 'store', // 有REMOVED標誌,這裡不關注 withRef = false, // 是否通過 forwardRef 暴露出傳入的Component的DOM forwardRef = false, // React的createContext context = ReactReduxContext, // 其餘的(比較方法,引數處理方法等)將會傳遞給上面的 selectFactory ...connectOptions } = {} ) { /* ... */ }
引數也沒什麼特別的,有一個 forwardRef
作用就是能獲取到我們傳入的 Component
的DOM。
這裡也不深入。
接著看
export default function connectAdvanced( /* 上面已講 */ ) { /* ...對引數的一些驗證和提示哪些引數已經作廢... */ // 定義Context const Context = context return function wrapWithConnect(WrappedComponent) { /* ...檢查 WrappedComponent 是否符合要求... */ /* ...獲取傳入的WrappedComponent的名稱... */ /* ...通過WrappedComponent的名稱計算出當前HOC的名稱... */ /* ...獲取一些上面的引數(沒有新的引數,都是之前見過的)... */ // Component就是React.Component let OuterBaseComponent = Component let FinalWrappedComponent = WrappedComponent // 是否純元件 if (pure) { OuterBaseComponent = PureComponent } /* 定義 makeDerivedPropsSelector 方法,作用後面講 */ /* 定義 makeChildElementSelector 方法,作用後面講 */ /* 定義 Connect 元件,作用後面講 */ Connect.WrappedComponent = WrappedComponent Connect.displayName = displayName /* ...如果是forWardRef 為true的情況,此處不深入... */ // 靜態方法轉換 return hoistStatics(Connect, WrappedComponent) } }
這一段特別長,因此我將不太重要的直接用註釋說明了它們在做什麼,具體程式碼就不放了(不重要)。
並且定義了3個新東西, makeDerivedPropsSelector
, makeChildElementSelector
, Connect
。
先看最後一句 hoistStatics
就是 hoist-non-react-statics
,它的作用是將元件 WrappedComponent
的所有非 React
靜態方法傳遞到 Connect
內部。
那麼最終它還是返回了一個 Connect
元件。
Connect元件
這個元件已經是我們寫了完整 connect(...)(Component)
的返回值了,所以能確定,只要呼叫 <Connect />
,就能渲染出一個新的元件出來。
因此它的功能就是確定是否重複更新元件和確定到底更新什麼?
看一個元件,從 constructor
看起
class Connect extends OuterBaseComponent { constructor(props) { super(props) /* ...提示一些無用的引數...*/ this.selectDerivedProps = makeDerivedPropsSelector() this.selectChildElement = makeChildElementSelector() this.renderWrappedComponent = this.renderWrappedComponent.bind(this) } /* ... */ }
綁定了一個方法,看名字是render的意思,先不管它。
執行了2個函式。
Connect
元件還沒完,這裡先放著,我們先看 makeDerivedPropsSelector
和 makeChildElementSelector
makeDerivedPropsSelector
function makeDerivedPropsSelector() { // 閉包儲存上一次的執行結果 let lastProps let lastState let lastDerivedProps let lastStore let sourceSelector return function selectDerivedProps(state, props, store) { // props和state都和之前相等 直接返回上一次的結果 if (pure && lastProps === props && lastState === state) { return lastDerivedProps } // 當前store和lastStore不等,更新lastStore if (store !== lastStore) { lastStore = store // 終於呼叫 selectorFactory 了 sourceSelector = selectorFactory( store.dispatch, selectorFactoryOptions ) } // 更新資料 lastProps = props lastState = state // 返回的就是最終的包含所有相應的 state 和 props 的結果 const nextProps = sourceSelector(state, props) // 最終的比較 if (lastDerivedProps === nextProps) { return lastDerivedProps } lastDerivedProps = nextProps return lastDerivedProps } }
大概的說, makeDerivedPropsSelector
的執行,先判斷了當前傳入的 props(元件的props)
和 state(redux傳入的state)
跟以前的是否全等,如果全等就不需要更新了;
如果不等,則呼叫了高階函式 selectFactory
,並且獲得最終資料,最後再判斷最終資料和之前的最終資料是否全等。
為什麼第一次判斷了,還要判斷第二次,而且都是 ===
判斷?
因為第一次獲取的 state
是 redux
傳入的,是整個APP的所有資料,它們不等說明有元件更新了,但不確定是否是當前元件;
第二次比較的是當前元件的最新資料和以前資料對比。
現在,我們知道 selectFactory
的作用是獲取當前元件的的最新資料,深入原始碼看看。
selectFactory
export default function finalPropsSelectorFactory( // redux store的store.dispatch dispatch, // 3種已經確定了的處理方法 { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) { // 返回一個針對使用者傳入的型別的解析函式 // 例如 mapStateToProps 如果是function,那麼就返回proxy,proxy可以判斷是否需要ownProps,並且對高階函式的 mapStateToProps 進行2次處理, // 最終確保返回一個plainObject,否則報錯 const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options) if (process.env.NODE_ENV !== 'production') { verifySubselectors( mapStateToProps, mapDispatchToProps, mergeProps, options.displayName ) } const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory // 預設pure問題true,因此執行 pureFinalPropsSelectorFactory(...) return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options ) }
引數就不說了,看註釋。
以下3個,到底返回了什麼,原始碼在 wrapMapToProps.js
,也說過這個檔案內部做了什麼事情。
const mapStateToProps = initMapStateToProps(dispatch, options) const mapDispatchToProps = initMapDispatchToProps(dispatch, options) const mergeProps = initMergeProps(dispatch, options)
這3個呼叫返回的一個函式,名字叫 proxy
,這個 proxy
一旦呼叫,
就能返回經過 mapStateToProps
, mapDispatchToProps
, mergeProps
這3個引數處理過後的資料( plainObject
)。
接下來:
const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory // 預設pure問題true,因此執行 pureFinalPropsSelectorFactory(...) return selectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, options )
返回了 selectorFactory
的呼叫值,也就是 pureFinalPropsSelectorFactory
(pure預設為true)。
看 pureFinalPropsSelectorFactory
,它的程式碼不少,但邏輯很明瞭,大方向就是對比資料。
這裡關鍵的如何比較不列程式碼,只用註釋講明白它的邏輯。
export function pureFinalPropsSelectorFactory( // 接受3個proxy方法 mapStateToProps, mapDispatchToProps, mergeProps, dispatch, // 接受3個比較方法 { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } ) { /* ...定義變數儲存之前的資料(閉包)... */ function handleFirstCall(firstState, firstOwnProps) { /* ...定義第一次執行資料比較的方法,也就是簡單的賦值給上面定義的閉包變數... */ } function handleNewPropsAndNewState() { /* 當state和props都有變動時的處理方法 */ } function handleNewProps() { /* 當state無變動,props有變動時的處理方法 */ } function handleNewState() { /* 當state有變動,props無變動時的處理方法 */ } // 後續資料比較的方法 function handleSubsequentCalls(nextState, nextOwnProps) { // 淺比較 const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) // 全等比較 const stateChanged = !areStatesEqual(nextState, state) // 更新資料 state = nextState ownProps = nextOwnProps // 當發生不相等的3種情況(關鍵) if (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) } }
上面的閉包變數儲存了上一次的資料,關鍵點就是當和這一次的資料比較後,如果處理更新。
react-redux
將它分為3種情況
-
state
和props
都相等。 -
state
相等,props
不等。 -
state
不等,props
相等。
-
第一種:
state
和props
都相等- mapStateToProps(proxy):
不管是否訂閱
ownProps
,執行mapStateToProps
, 因為state
有變動。 - mapDispatchToProps(proxy):
只有訂閱了
ownProps
,才會執行mapDispatchToProps
,因為state
變動與mapDispatchToProps
無影響。 -
mergedProps(proxy):
必定執行,將所有結果合並。
- mapStateToProps(proxy):
-
第二種:
state
相等,props
不等- mapStateToProps(proxy):
只有訂閱了
ownProps
,才會執行mapStateToProps
, 因為state
無變動。 - mapDispatchToProps(proxy):
只有訂閱了
ownProps
,才會執行mapDispatchToProps
,因為state
變動與mapDispatchToProps
無影響。 -
mergedProps(proxy):
必定執行,將所有結果合併。
- mapStateToProps(proxy):
-
第三種:
state
不等,props
相等- mapStateToProps(proxy):
不管是否訂閱
ownProps
,執行mapStateToProps
, 因為state
有變動。注意,這裡結果需要
淺比較
判斷因為如果沒有
淺比較
檢查,而兩者剛好淺比較相等
,那麼最後也會認為返回一個新的props,也就是相當於重複更新了。
之所以第一個
state
和props
都有變動的不需要淺比較檢查,是因為如果
props
變了,則必須要更新元件。 - mapDispatchToProps(proxy):
不會執行,因為它只關注
props
。 -
mergedProps(proxy):
只有上面淺比較不等,才會執行。
- mapStateToProps(proxy):
makeDerivedPropsSelector
的總結:
通過閉包管理資料,並且通過淺比較和全等比較判斷是否需要更新元件資料。
makeChildElementSelector
makeChildElementSelector
也是一個高階函式,儲存了之前的 資料
和 元件
,並且判斷與當前的判斷。
這裡是最終渲染元件的地方,因為需要判斷一下剛才最終給出的資料是否需要去更新元件。
2個邏輯:
=== forWardRef
否則,返回舊元件(不更新)。
繼續回到 Connect
元件。
之後就是 render
了
render() { // React的createContext const ContextToUse = this.props.context || Context return ( <ContextToUse.Consumer> {this.renderWrappedComponent} </ContextToUse.Consumer> ) }
Context.Consumer
內部必須是一個函式,這個函式的引數就是 Context.Provider
的 value
,也就是 redux
的 store
。
renderWrappedComponent
最後一個函式: renderWrappedComponent
renderWrappedComponent(value) { /* ...驗證引數有效性... */ // 這裡 storeState=store.getState() const { storeState, store } = value // 傳入自定義元件的props let wrapperProps = this.props let forwardedRef if (forwardRef) { wrapperProps = this.props.wrapperProps forwardedRef = this.props.forwardedRef } // 上面已經講了,返回最終資料 let derivedProps = this.selectDerivedProps( storeState, wrapperProps, store ) // 返回最終渲染的自定義元件 return this.selectChildElement(derivedProps, forwardedRef) }
總算結束了,可能有點混亂,做個總結吧。
總結
我把 react-redux
的執行流程分為3個階段,分別對應我們的程式碼編寫(搭配導圖閱讀)
一張導圖:
第一階段:
對應的使用者程式碼:
<Provider store={store}> <App /> </Provider>
執行內容有:
- 定義了
Provider
元件,這個元件內部訂閱了redux
的store
,保證當store
發生變動,會立刻執行更新。
第二階段:
對應的使用者程式碼:
connect(mapStateToProps,mapDispatchToProps,mergeProps,options)
執行內容有:
-
connect
接收了引數(mapStateToProps
,mapDispatchToProps
,mergeProps
,options
)。 -
connect
接收了對引數如何處理方案(3個...Factories
)。 -
connect
接收了引數的結果比較方案(selectFactory
) - 定義了比較方案(4個
are...Equal
,其實就是全等比較
和淺比較
)。
第三階段:
對應的使用者程式碼:
let newComponent=connect(...)(Component) <newComponent />
執行內容有:
- 接受自定義元件(
Component
)。 - 建立一個
Connect
元件。 - 將
Component
的非React
靜態方法轉移到Connect
。 - 獲取
Provider
傳入的資料
(redux
的整個資料),利用閉包儲存資料,用於和未來資料做比較。 - 當比較(
===
)有變動,執行上一階段傳入的引數,獲取當前元件真正的資料。 - 利用閉包儲存當前元件真正的資料,用於和未來作比較。
- 通過全等和淺比較,處理
state
變動和props
變動的邏輯,判斷返回新資料還是舊資料。 - 利用閉包儲存渲染的元件,通過上面返回的最終資料,判斷需要返回新元件還是就元件。
邏輯理順了,還是很好理解的。
其中第三階段就是對外API connectAdvanced
的執行內容。
ofollow,noindex" target="_blank">此處 檢視更多前端原始碼閱讀內容。
或許哪一天,我們需要設計一個專用的資料管理系統,那麼就利用好 connectAdvanced
,
我們要做的就是編寫一個自定義 第二階段
的邏輯體系。
感謝閱讀!