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(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
變動的邏輯,判斷返回新數據還是舊數據。 - 利用閉包保存渲染的組件,通過上面返回的最終數據,判斷需要返回新組件還是就組件。
邏輯理順了,還是很好理解的。
其中第三階段就是對外APIconnectAdvanced
的執行內容。
此處查看更多前端源碼閱讀內容。
或許哪一天,我們需要設計一個專用的數據管理系統,那麼就利用好connectAdvanced
,
我們要做的就是編寫一個自定義第二階段
的邏輯體系。
感謝閱讀!