使用過redux
的同窗都知道,redux
做爲react
公共狀態管理工具,配合react-redux
能夠很好的管理數據,派發更新,更新視圖渲染的做用,那麼對於 react-redux
是如何作到根據 state
的改變,而更新組件,促使視圖渲染的呢,讓咱們一塊兒來探討一下,react-redux
源碼的奧妙所在。前端
在正式分析以前咱們不妨來想幾個問題:
vue
1 爲何要在 root
根組件上使用 react-redux
的 Provider
組件包裹?
2 react-redux
是怎麼和 redux
契合,作到 state
改變動新視圖的呢?
3 provide
用什麼方式存放當前的 redux
的 store
, 又是怎麼傳遞給每個須要管理state
的組件的?
4 connect
是怎麼樣鏈接咱們的業務組件,而後傳遞咱們組件更新函數的呢?
5 connect
是怎麼經過第一個參數,來訂閱與之對應的 state
的呢?
6 connect
怎麼樣將 props
,和 redux
的 state
合併的?react
帶着這些疑問咱們不妨先看一下 Provider
究竟作了什麼?算法
/* provider 組件代碼 */
function Provider({ store, context, children }) {
/* 利用useMemo,跟據store變化建立出一個contextValue 包含一個根元素訂閱器和當前store */
const contextValue = useMemo(() => {
/* 建立了一個根 Subscription 訂閱器 */
const subscription = new Subscription(store)
/* subscription 的 notifyNestedSubs 方法 ,賦值給 onStateChange方法 */
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
} /* store 改變建立新的contextValue */
}, [store])
/* 獲取更新以前的state值 ,函數組件裏面的上下文要優先於組件更新渲染 */
const previousState = useMemo(() => store.getState(), [store])
useEffect(() => {
const { subscription } = contextValue
/* 觸發trySubscribe方法執行,建立listens */
subscription.trySubscribe() // 發起訂閱
if (previousState !== store.getState()) {
/* 組件更新渲染以後,若是此時state發生改變,那麼當即觸發 subscription.notifyNestedSubs 方法 */
subscription.notifyNestedSubs()
}
/* */
return () => {
subscription.tryUnsubscribe() // 卸載訂閱
subscription.onStateChange = null
}
/* contextValue state 改變出發新的 effect */
}, [contextValue, previousState])
const Context = context || ReactReduxContext
/* context 存在用跟元素傳進來的context ,若是不存在 createContext建立一個context ,這裏的ReactReduxContext就是由createContext建立出的context */
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
複製代碼
1 首先建立一個 contextValue
,裏面包含一個建立出來的父級 Subscription
(咱們姑且先稱之爲根級訂閱器)和redux
提供的store
。
2 經過react上下文context
把 contextValue
傳遞給子孫組件。redux
在咱們分析了不是很長的 provider
源碼以後,隨之一個 Subscription
出現,那麼這個 Subscription
由什麼做用呢🤔🤔🤔,咱們先來看看在 Provder
裏出現的Subscription
方法。小程序
notifyNestedSubs
trySubscribe
tryUnsubscribe
設計模式
在整個 react-redux
執行過程當中 Subscription
做用很是重要,這裏方便先透漏一下,他的做用是收集全部被 connect
包裹的組件的更新函數 onstatechange
,而後造成一個 callback
鏈表,再由父級 Subscription
統一派發執行更新,咱們暫且不關心它是怎麼運做的,接下來就是 Subscription
源碼 ,咱們重點看一下如上出現的三個方法。前端工程化
/* 發佈訂閱者模式 */
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
/* 負責檢測是否該組件訂閱,而後添加訂閱者也就是listener */
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
/* 向listeners發佈通知 */
notifyNestedSubs() {
this.listeners.notify()
}
/* 對於 provide onStateChange 就是 notifyNestedSubs 方法,對於 connect 包裹接受更新的組件 ,onStateChange 就是 負責更新組件的函數 。 */
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
/* 判斷有沒有開啓訂閱 */
isSubscribed() {
return Boolean(this.unsubscribe)
}
/* 開啓訂閱模式 首先判斷當前訂閱器有沒有父級訂閱器 , 若是有父級訂閱器(就是父級Subscription),把本身的handleChangeWrapper放入到監聽者鏈表中 */
trySubscribe() {
/* parentSub 便是provide value 裏面的 Subscription 這裏能夠理解爲 父級元素的 Subscription */
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
/* provider的Subscription是不存在parentSub,因此此時trySubscribe 就會調用 store.subscribe */
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
/* 取消訂閱 */
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
複製代碼
看完 Provider
和 Subscription
源碼,我來解釋一下二者到底有什麼關聯,首先Provider
建立 Subscription
時候沒有第二個參數,就說明provider
中的Subscription
不存在 parentSub
。 那麼再調用Provider
組件中useEffect
鉤子中trySubscribe
的時候,會觸發this.store.subscribe
, subscribe
就是 redux
的 subscribe
,此時真正發起了訂閱。數組
subscription.onStateChange = subscription.notifyNestedSubs
複製代碼
有此可知,最終state
改變,觸發的是notifyNestedSubs
方法。咱們再一次看看這個notifyNestedSubs
。緩存
/* 向listeners發佈通知 */
notifyNestedSubs() {
this.listeners.notify()
}
複製代碼
最終向當前Subscription
的訂閱者們發佈 notify
更新。
綜上所述咱們總結一下。Subscription
的做用,首先經過 trySubscribe
發起訂閱模式,若是存在這父級訂閱者,就把本身更新函數handleChangeWrapper
,傳遞給父級訂閱者,而後父級由 addNestedSub
方法將此時的回調函數(更新函數)添加到當前的 listeners
中 。若是沒有父級元素(Provider
的狀況),則將此回調函數放在store.subscribe
中,handleChangeWrapper
函數中onStateChange
,就是 Provider
中 Subscription
的 notifyNestedSubs
方法,而 notifyNestedSubs
方法會通知listens
的 notify
方法來觸發更新。這裏透漏一下,子代Subscription
會把更新自身handleChangeWrapper
傳遞給parentSub
,來統一通知connect
組件更新。
這裏咱們弄明白一個問題
react-redux
更新組件也是用了 store.subscribe
並且 store.subscribe
只用在了 Provider
的 Subscription
中 (沒有 parentsub
)
大體模型就是
state
更改 -> store.subscribe
-> 觸發 provider
的 Subscription
的 handleChangeWrapper
也就是 notifyNestedSubs
-> 通知 listeners.notify()
-> 通知每一個被 connect
容器組件的更新 -> callback
執行 -> 觸發子組件Subscription
的 handleChangeWrapper ->觸發子 onstatechange
(能夠提早透漏一下,onstatechange
保存了更新組件的函數)。
前邊的內容提到了**createListenerCollection
,listeners
**,可是他具體有什麼做用咱們接下來一塊兒看一下。
function createListenerCollection() {
/* batch 由getBatch獲得的 unstable_batchedUpdates 方法 */
const batch = getBatch()
let first = null
let last = null
return {
/* 清除當前listeners的全部listener */
clear() {
first = null
last = null
},
/* 派發更新 */
notify() {
batch(() => {
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
})
},
/* 獲取listeners的全部listener */
get() {
let listeners = []
let listener = first
while (listener) {
listeners.push(listener)
listener = listener.next
}
return listeners
},
/* 接收訂閱,將當前的callback(handleChangeWrapper)存到當前的鏈表中 */
subscribe(callback) {
let isSubscribed = true
let 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) return
isSubscribed = false
if (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)
複製代碼
咱們能夠得出結論 createListenerCollection
能夠產生一個 listeners
。 listeners
的做用。
1收集訂閱: 以鏈表的形式收集對應的 listeners
(每個Subscription
) 的handleChangeWrapper
函數。
2派發更新:, 經過 batch
方法( react-dom
中的 unstable_batchedUpdates
) 來進行批量更新。
舒適提示: React
的 unstable_batchedUpdate()
API
容許將一次事件循環中的全部 React 更新都一塊兒批量處理到一個渲染過程當中。
🤔到這裏咱們明白了:
1 react-redux
中的 provider
做用 ,經過 react
的 context
傳遞 subscription
和 redux
中的store
,而且創建了一個最頂部根 Subscription
。
2 Subscription
的做用:起到發佈訂閱做用,一方面訂閱 connect
包裹組件的更新函數,一方面經過 store.subscribe
統一派發更新。
3 Subscription
若是存在這父級的狀況,會把自身的更新函數,傳遞給父級 Subscription
來統一訂閱。
工慾善其事,必先利其器
,想要吃透源碼以前,必須深度熟悉其用法。才能知其然知其因此然。咱們先來看看高階組件connect
用法。
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 複製代碼
mapStateToProps
const mapStateToProps = state => ({ todos: state.todos })
複製代碼
做用很簡單,組件依賴redux
的 state
,映射到業務組件的 props
中,state
改變觸發,業務組件props
改變,觸發業務組件更新視圖。當這個參數沒有的時候,當前組件不會訂閱 store
的改變。
mapDispatchToProps
const mapDispatchToProps = dispatch => {
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' })
}
}
複製代碼
將 redux
中的dispatch
方法,映射到,業務組件的props
中。
mergeProps
/* * stateProps , state 映射到 props 中的內容 * dispatchProps, dispatch 映射到 props 中的內容。 * ownProps 組件自己的 props */
(stateProps, dispatchProps, ownProps) => Object
複製代碼
正常狀況下,若是沒有這個參數,會按照以下方式進行合併,返回的對象能夠是,咱們自定義的合併規則。咱們還能夠附加一些屬性。
{ ...ownProps, ...stateProps, ...dispatchProps }
複製代碼
options
{
context?: Object, // 自定義上下文
pure?: boolean, // 默認爲 true , 當爲 true 的時候 ,除了 mapStateToProps 和 props ,其餘輸入或者state 改變,均不會更新組件。
areStatesEqual?: Function, // 當pure true , 比較引進store 中state值 是否和以前相等。 (next: Object, prev: Object) => boolean
areOwnPropsEqual?: Function, // 當pure true , 比較 props 值, 是否和以前相等。 (next: Object, prev: Object) => boolean
areStatePropsEqual?: Function, // 當pure true , 比較 mapStateToProps 後的值 是否和以前相等。 (next: Object, prev: Object) => boolean
areMergedPropsEqual?: Function, // 當 pure 爲 true 時, 比較 通過 mergeProps 合併後的值 , 是否與以前等 (next: Object, prev: Object) => boolean
forwardRef?: boolean, //當爲true 時候,能夠經過ref 獲取被connect包裹的組件實例。
}
複製代碼
options
能夠是如上屬性,上面已經標註了每個屬性的做用,這裏就很少說了。
對於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 } = {} ) {
/* 通過代理包裝後的 mapStateToProps */
const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories,'mapStateToProps' )
/* 通過代理包裝後的 mapDispatchToProps */
const initMapDispatchToProps = match( mapDispatchToProps, mapDispatchToPropsFactories,'mapDispatchToProps')
/* 通過代理包裝後的 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()
複製代碼
咱們先來分析一下整個函數作的事。
1 首先定一個 createConnect
方法。 傳入了幾個默認參數,有兩個參數很是重要,connectHOC
做爲整個 connect
的高階組件。selectorFactory
作爲整合connect
更新過程當中的造成新props
的主要函數。默認的模式是pure
模式。
2 而後執行createConnect
方法,返回真正的connect
函數自己。connect
接收幾個參數,而後和默認的函數進行整合,包裝,代理,最後造成三個真正的初始化函數,這裏的過程咱們就先不講了。咱們接下來分別介紹這三個函數的用途。
initMapStateToProps ,用於造成真正的 MapStateToProps
函數,將 store 中 state ,映射到 props
initMapDispatchToProps,用於造成真正的 MapDispatchToProps
,將 dispatch
和 自定義的 dispatch
注入到props
。
initMergeProps,用於造成真正的 mergeProps
函數,合併業務組件的 props
, state
映射的 props
, dispatch
映射的 props
。
這裏有一個函數很是重要,這個函數就是mergeProps
, 請你們記住這個函數,由於這個函數是判斷整個connect
是否更新組件關鍵所在。上邊說過 connect
基本用法的時候說過,當咱們不向connect
傳遞第三個參數mergeProps
的時候,默認的defaultMergeProps
以下
/src/connect/mergeProps.js
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
複製代碼
這個函數返回了一個新的對象,也就是新的props
。並且將 業務組件 props
, store
中的 state
,和 dispatch
結合到一塊兒,造成一個新對象,做爲新的 props
傳遞給了業務組件。
前面說到selectorFactory
很重要,用於造成新的props
,咱們記下來看selectorFactory
源碼。
/src/connect/selectorFactory.js
export default function finalPropsSelectorFactory( dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) {
// mapStateToProps mapDispatchToProps mergeProps 爲真正connect 通過一層代理的 proxy 函數
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory
// 返回一個 函數用於生成新的 props
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
複製代碼
finalPropsSelectorFactory
的代碼很簡單, 首先獲得真正connect 通過一層代理函數 mapStateToProps
,mapDispatchToProps
,mergeProps
。而後調用selectorFactory
(在pure
模式下,selectorFactory
就是 pureFinalPropsSelectorFactory
) 。
能夠這裏反覆用了閉包,能夠剛開始有點蒙,不過靜下心來看發現其實不是很難。因爲默認是pure
,因此咱們接下來主要看 pureFinalPropsSelectorFactory 函數作了些什麼。
/** pure組件處理 , 對比 props 是否發生變化 而後 合併props */
export function pureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } //判斷 state prop 是否相等 ) {
let hasRunAtLeastOnce = false
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
/* 第一次 直接造成 ownProps stateProps dispatchProps 合併 造成新的 props */
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
function handleNewPropsAndNewState() {
// props 和 state 都改變 mergeProps
}
function handleNewProps() {
// props 改變 mergeProps
}
function handleNewState() {
// state 改變 mergeProps
}
/* 不是第一次的狀況 props 或者 store.state 發生改變的狀況。 */
function handleSubsequentCalls(nextState, nextOwnProps) {
/* 判斷兩次 props 是否相等 */
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
/* 判斷兩次 store.state 是否相等 */
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
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)
}
}
複製代碼
這個函數處理邏輯很清晰。大體上作了這些事。經過閉包的形式返回一個函數pureFinalPropsSelector
。pureFinalPropsSelector
經過判斷是不是第一次初始化組件。
若是是第一次,那麼直接調用mergeProps
合併ownProps
,stateProps
,dispatchProps
造成最終的props
。 若是不是第一次,那麼判斷究竟是props
仍是 store.state
發生改變,而後針對那裏變化,從新生成對應的props
,最終合併到真正的props
。
整個 selectorFactory
邏輯就是造成新的props
傳遞給咱們的業務組件。
接下來咱們看一下 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
執行 接受 mapStateToProp
等參數,最後返回 connectAdvanced()
,那麼上述例子中connect
執行第一步connect(mapStateToProp)===connectAdvanced()
,也就是connectAdvanced()
執行返回真正的hoc
,用於包裹咱們的業務組件。
接下來咱們看 connectAdvanced
代碼
/src/components/connectAdvanced.js
export default function connectAdvanced( selectorFactory, // 每次 props,state改變執行 ,用於生成新的 props。 { getDisplayName = name => `ConnectAdvanced(${name})`, //可能被包裝函數(如connect())重寫 methodName = 'connectAdvanced', //若是定義了,則傳遞給包裝元素的屬性的名稱,指示要呈現的調用。用於監視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 的業務組件自己
}
}
複製代碼
connectAdvanced
接受配置參數 , 而後返回真正的 HOC
wrapWithConnect
。
// 咱們能夠講下面的表達式分解
connect(mapStateToProp)(Index)
// 執行 connect
connect(mapStateToProp)
//返回
connectAdvanced()
//返回HOC
wrapWithConnect
複製代碼
接下來咱們分析一下wrapWithConnect
到底作了些什麼?
接下來咱們來一塊兒研究一下 wrapWithConnect
,咱們重點看一下 wrapWithConnect
做爲高階組件,會返回一個組件,這個組件會對原有的業務組件,進行一系列加強等工做。
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 } = connectOptions
function createChildSelector(store) {
// 合併函數 mergeprops 獲得最新的props
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
//判斷是不是pure純組件模式 若是是 將用 useMemo 提高性能
const usePureOnlyMemo = pure ? useMemo : callback => callback()
// 負責更新的容器子組件
function ConnectFunction (props){
// props 爲 業務組件 真正的 props
}
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
/* forwardRef */
if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef( props, ref ) {
return <Connect {...props} reactReduxForwardedRef={ref} />
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
return hoistStatics(Connect, WrappedComponent)
}
}
複製代碼
wrapWithConnect
的作的事大體分爲一下幾點:
1 聲明負責更新的 ConnectFunction
無狀態組件。和負責合併 props
的createChildSelector
方法
2 判斷是不是 pure
純組件模式,若是是用react.memo
包裹,這樣作的好處是,會向 pureComponent
同樣對 props
進行淺比較。
3 若是 connect
有forwardRef
配置項,用React.forwardRef
處理,這樣作好處以下。
正常狀況下由於咱們的WrappedComponent
被 connect
包裝,因此不能經過ref
訪問到業務組件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 = null
render(){
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
的靜態方法/屬性,繼承到父組件Connect
上。由於在 高階組件 包裝 業務組件的過程當中,若是不對靜態屬性或是方法加以額外處理,是不會被包裝後的組件訪問到的,因此須要相似hoistStatics
這樣的庫,來作處理。
接下來說的就是整個 connect
的核心了。咱們來看一下負責更新的容器ConnectFunction
到底作了些什麼?
ConnectFunction
的代碼很複雜,須要咱們一步步去吃透,一步步去消化。
function ConnectFunction(props) {
/* TODO: 第一步 把 context ForwardedRef props 取出來 */
const [
reactReduxForwardedRef,
wrapperProps // props 傳遞的props
] = useMemo(() => {
const { reactReduxForwardedRef, ...wrapperProps } = props
return [reactReduxForwardedRef, wrapperProps]
}, [props])
// 獲取 context內容 裏面含有 redux 中store 和 subscription
const contextValue = useContext(Context)
//TODO: 判斷 store 是否來此 props didStoreComeFromProps ,正常狀況下 ,prop 中是不存在 store 因此 didStoreComeFromProps = false
const didStoreComeFromProps =
Boolean(props.store) &&
Boolean(props.store.getState) &&
Boolean(props.store.dispatch)
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
// 獲取 redux 中 store
const store = didStoreComeFromProps ? props.store : contextValue.store
// 返回merge函數 用於生成真正傳給子組件 props
const childPropsSelector = useMemo(() => {
return createChildSelector(store)
}, [store])
// TODO: 第二步 subscription 監聽者實例
const [subscription, notifyNestedSubs] = useMemo(() => {
// 若是沒有訂閱更新,那麼直接返回。
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription // 和 上級 `subscription` 創建起關係。 this.parentSub = contextValue.subscription
)
// notifyNestedSubs 觸發 noticy 全部子代 listener 監聽者 -> 觸發batch方法,觸發 batchupdate方法 ,批量更新
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
/* 建立出一個新的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信息(通過 ownprops ,stateProps , dispatchProps 合併過的 )
const lastWrapperProps = useRef(wrapperProps) // 保存本次上下文執行 業務組件的 props
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false) // 當前組件是否處於渲染階段
// actualChildProps 爲當前真正處理事後,通過合併的 props
const actualChildProps = usePureOnlyMemo(() => {
// 調用 mergeProps 進行合併,返回合併後的最新 porps
return 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是否有第一個參數
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
函數childPropsSelector
。
subscription
, 層層傳遞新的 context
(很重要)這一步很是重要,判斷經過shouldHandleStateChanges
判斷此 HOC
是否訂閱存儲更改,若是已經訂閱了更新(此時connect
具備第一個參數),那麼建立一個 subscription
,而且和上一層provider
的subscription
創建起關聯。this.parentSub = contextValue.subscription
。而後分離出 subscription
和 notifyNestedSubs
(notifyNestedSubs
的做用是通知當前subscription
的 listeners
進行更新的方法。 ) 。
而後經過 useMemo
建立出一個新的 contextValue
,把父級的 subscription
換成本身的 subscription
。用於經過 Provider
傳遞新的 context
。這裏簡單介紹一下,運用了 Provider
能夠和多個消費組件有對應關係。多個 Provider
也能夠嵌套使用,裏層的會覆蓋外層的數據。react-redux
用context
更傾向於Provider
良好的傳遞上下文的能力。
接下來經過useReducer
製造出真正觸發更新的forceComponentUpdateDispatch
函數。也就是整個 state
或者是 props
改變,觸發組件更新的函數。 爲何這麼作呢?
筆者認爲react-redxx
這樣設計緣由是但願connect
本身控制本身的更新,而且多個上下級 connect
不收到影響。因此一方面經過useMemo
來限制業務組件沒必要要的更新,另外一方面來經過forceComponentUpdateDispatch
來更新 HOC
函數,產生actualChildProps
,actualChildProps
改變 ,useMemo
執行,觸發組件渲染。
這一步十分重要,爲何這麼說呢,首先先經過useRef
緩存幾個變量:
lastChildProps -> 保存上一次 合併過的 props
信息(通過 ownprops
,stateProps
, dispatchProps
合併過的 )。 lastWrapperProps -> 保存本次上下文執行 業務組件的 props
。 renderIsScheduled -> 當前組件是否處於渲染階段。 actualChildProps -> actualChildProps
爲當前真正處理事後,通過合併的 props
, 組件經過 dep
-> actualChildProps
,來判斷是否進行更新。
接下來執行兩次 useEffect
, 源碼中不是這個樣子的,我這裏通過簡化,第一個 useEffect
執行了 captureWrapperProps
,captureWrapperProps
是幹什麼的呢?
//獲取包裝的props
function captureWrapperProps( lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs ) {
lastWrapperProps.current = wrapperProps //子props
lastChildProps.current = actualChildProps //通過 megeprops 以後造成的 prop
renderIsScheduled.current = false // 當前組件渲染完成
}
複製代碼
captureWrapperProps
的做用很簡單,在一次組件渲染更新後,將上一次 合併前 和 合併後 的props
,保存起來。這麼作目的是,能過在兩次hoc
執行渲染中,對比props stateProps
是否發生變化。從而肯定是否更新 hoc
,進一步更新組件。
執行第二個 useEffect
是很關鍵。執行subscribeUpdates
函數,subscribeUpdates
是訂閱更新的主要函數,咱們一塊兒來看看:
function subscribeUpdates( shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, //子props lastChildProps, //通過 megeprops 以後造成的 prop renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch ) {
if (!shouldHandleStateChanges) return
// 捕獲值以檢查此組件是否卸載以及什麼時候卸載
let didUnsubscribe = false
let lastThrownError = null
//store更新訂閱傳播到此組件時,運行此回調
const checkForUpdates = ()=>{
//....
}
subscription.onStateChange = checkForUpdates
//開啓訂閱者 ,當前是被connect 包轉的狀況 會把 當前的 checkForceUpdate 放在存入 父元素的addNestedSub中。
subscription.trySubscribe()
//在第一次呈現以後從存儲中提取數據,以防存儲從咱們開始就改變了。
checkForUpdates()
/* 卸載訂閱起 */
const unsubscribeWrapper = () => {
didUnsubscribe = true
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
return unsubscribeWrapper
}
複製代碼
這絕對是整個訂閱更新的核心,首先聲明 store
更新訂閱傳播到此組件時的回調函數checkForUpdates
把它賦值給onStateChange
,若是store
中的state
發生改變,那麼在組件訂閱了state
內容以後,相關聯的state
改變就會觸發當前組件的onStateChange
,來合併獲得新的props
,從而觸發組件更新。
而後subscription.trySubscribe()
把訂閱函數onStateChange
綁定給父級subscription
,進行了層層訂閱。
最後,爲了防止渲染後,store
內容已經改變,因此首先執行了一次checkForUpdates
。那麼checkForUpdates
的做用很明確了,就是檢查是否派發當前組件的更新。
到這裏咱們明白了,react-redux
經過 subscription
進行層層訂閱。對於一層層的組件結構,總體模型圖以下:。
接下來咱們看一下checkForUpdates
//store更新訂閱傳播到此組件時,運行此回調
const checkForUpdates = () => {
if (didUnsubscribe) {
//若是寫在了
return
}
// 獲取 store 裏state
const latestStoreState = store.getState()q
let newChildProps, error
try {
/* 獲得最新的 props */
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
}
//若是新的合併的 props沒有更改,則此處不作任何操做-層疊訂閱更新
if (newChildProps === lastChildProps.current) {
if (!renderIsScheduled.current) {
notifyNestedSubs() /* 通知子代 subscription 觸發 checkForUpdates 來檢查是否須要更新。 */
}
} else {
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// 此狀況 可能考慮到 代碼運行到這裏 又發生了 props 更新 因此觸發一個 reducer 來促使組件更新。
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error
}
})
}
}
複製代碼
checkForUpdates
經過調用 childPropsSelector
來造成新的props
,而後判斷以前的 prop
和當前新的 prop
是否相等。若是相等,證實沒有發生變化,無須更新當前組件,那麼經過調用notifyNestedSubs
來通知子代容器組件,檢查是否須要更新。若是不相等證實訂閱的store.state
發生變化,那麼當即執行forceComponentUpdateDispatch
來觸發組件的更新。
對於層層訂閱的結構,整個更新模型圖以下:
接下來咱們總結一下整個connect
的流程。咱們仍是從訂閱和更新兩個方向入手。
整個訂閱的流程是,若是被connect
包裹,而且具備第一個參數。首先經過context
獲取最近的 subscription
,而後建立一個新的subscription
,而且和父級的subscription
創建起關聯。當第一次hoc
容器組件掛在完成後,在useEffect
裏,進行訂閱,將本身的訂閱函數checkForUpdates
,做爲回調函數,經過trySubscribe
和this.parentSub.addNestedSub
,加入到父級subscription
的listeners
中。由此完成整個訂閱流程。
整個更新流程是,那state
改變,會觸發根訂閱器的store.subscribe
,而後會觸發listeners.notify
,也就是checkForUpdates
函數,而後checkForUpdates
函數首先根據mapStoretoprops
,mergeprops
等操做,驗證該組件是否發起訂閱,props
是否改變,並更新,若是發生改變,那麼觸發useReducer
的forceComponentUpdateDispatch
函數,來更新業務組件,若是沒有發生更新,那麼經過調用notifyNestedSubs
,來通知當前subscription
的listeners
檢查是否更新,而後盡心層層checkForUpdates
,逐級向下,藉此完成整個更新流程。
useMemo
用法思考?整個react-redux
源碼中,對於useMemo
用法仍是蠻多的,我總結了幾條,奉上🌹🌹:
react-redux
源碼中,多處應用了useMemo
依賴/緩存 屬性的狀況。這樣作的好處是隻有依賴項發生改變的時候,才更新新的緩存屬性/方法,好比 childPropsSelector
, subscription
, actualChildProps
等主要方法屬性。
react-redux
源碼中,經過 useMemo
來控制業務組件是否渲染。經過 actualChildProps
變化,來證實是否來自 **自身 props
** 或 訂閱的 state
的修改,來肯定是否渲染組件。
例子🌰:
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
複製代碼
但願這篇文章能讓屏幕前的你,對react-redux
的訂閱和更新流程有一個新的認識。送人玫瑰,手留餘香,閱讀的朋友能夠給筆者點贊,關注一波 ,陸續更新前端超硬核文章。
回看筆者往期高贊文章,有更多精彩內容等着你!
「react進階」年終送給react開發者的八條優化建議 800+
贊👍
Vue組件通訊方式及其應用場景總結 250+
贊👍
h5,小程序飛入購物車(拋物線繪製運動軌跡點) 300
贊 👍
vue3.0源碼系列
vue3.0 響應式原理(超詳細) 200+
贊👍
全面解析 vue3.0 diff算法 100+
贊👍
vue3.0 watch 和 computed源碼解析 30+
贊👍
react-hooks系列
玩轉react-hooks,自定義hooks設計模式及其實戰 150+
👍贊
react-hooks如何使用 70+
贊👍
開源項目系列
230+
贊 👍