帶著問題看React-Redux原始碼
React-Redux在專案中的應用
在這裡,我就預設大家已經會使用Redux了,它為我們的應用提供一個全域性的物件(store)來管理狀態。
那麼如何將Redux應用在React中呢?想一下,我們的最終目的是實現跨層級元件間通訊與狀態的統一管理。所以可以使用Context這個特性。
- 建立一個Provider,將store傳入Provider,作為當前context的值,便於元件通過context獲取Redux store
- store訂閱一個元件更新的統一邏輯
- 元件需要更新資料時,需要呼叫store.dispatch派發action,進而觸發訂閱的更新
- 元件獲取資料時候,使用store.getState()獲取資料
而這些都需要自己手動去做,React-Redux將上邊的都封裝起來了。讓我們通過一段程式碼看一下React-Redux的用法:
首先是在React的最外層應用上,包裹Provider,而Provider是React-Redux提供的元件,這裡做的事情相當於上邊的第一步
import React from 'react'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
const reducer = (state, actions) => {
...
}
const store = createStore(reducer)
...
class RootApp extends React.Component {
render() {
// 這裡將store傳入Provider
return <Provider store={store}>
<App/>
</Provider>
}
}
第二步中的訂閱,已經分別在Provider和connect中實現了
再看應用內的子元件。如果需要從store中拿資料或者更新store資料的話(相當於上邊的第三步和第四步),
需要用connect將元件包裹起來:
import React from 'react'
import { connect } from '../../react-redux-src'
import { increaseAction, decreaseAction } from '../../actions/counter'
import { Button } from 'antd'
class Child extends React.Component {
render() {
const { increaseAction, decreaseAction, num } = this.props
return <div >
{num}
<Button onClick={() => increaseAction()}>增加</Button>
<Button onClick={() => decreaseAction()}>減少</Button>
</div>
}
}
const mapStateToProps = (state, ownProps) => {
const { counter } = state
return {
num: counter.num
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
increaseAction: () => dispatch({
type: INCREASE
}),
decreaseAction: () => dispatch({
type: DECREASE
})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Child)
mapStateToProps 用於建立元件和store中儲存的狀態的對映關係,它是一個函式,第一個引數是state,也就是redux中儲存的頂層資料,第二個引數是元件自身的props。返回一個物件,物件內的欄位就是該元件需要從store中獲取的值。
mapDispatchToProps用於建立元件和store.dispatch的對映關係。它可以是一個物件,也可以是一個函式,
當它是一個函式的時候,第一個引數就是dispatch,第二個引數是元件自身的props。
mapDispatchToProps的物件形式如下:
const mapDispatchToProps = {
increaseAction() {
return dispatch => dispatch({
type: INCREASE
})
},
decreaseAction() {
return dispatch => dispatch({
type: DECREASE
})
}
}
當不傳mapStateToProps的時候,當store變化的時候,不會引起元件UI的更新。
當不傳mapDispatchToProps的時候,預設將dispatch注入到元件的props中。
以上,如果mapStateToProps 或者mapDispatchToProps傳了ownProps,那麼在元件自身的props變化的時候,這兩個函式也都會被呼叫。
React-Redux做了什麼
我們先給出結論,說明React-Redux做了什麼工作:
- 提供Subscrption類,實現訂閱更新的邏輯
- 提供Provider,將store傳入Provider,便於下層元件從context或者props中獲取store;並訂閱store的變化,便於在store變化的時候更新Provider自身
- 提供selector,負責將獲取store中的stat和dispacth一些action的函式(或者直接就是dispatch)或者元件自己的props,並從中選擇出元件需要的值,作為返回值
-
提供connect高階元件,主要做了兩件事:
- 執行selector,獲取到要注入到元件中的值,將它們注入到元件的props
- 訂閱props的變化,負責在props變化的時候更新元件
如何做的
有了上邊的結論,但想必大家都比較好奇究竟是怎麼實現的,上邊的幾項工作都是協同完成的,最終的表象體現為下面幾個問題:
- Provider是怎麼把store放入context中的
- 如何將store中的state和dispatch(或者呼叫dispatch的函式)注入元件的props中的
- 我們都知道在Redux中,可以通過store.subscribe()訂閱一個更新頁面的函式,來實現store變化,更新UI,而React-Redux是如何做到store變化,被connect的元件也會更新的
接下來,帶著這些問題來一條一條地分析原始碼。
Provider是怎麼把store放入context中的
先從Provider元件入手,程式碼不多,直接上原始碼
class Provider extends Component {
constructor(props) {
super(props)
// 從props中取出store
const { store } = props
this.notifySubscribers = this.notifySubscribers.bind(this)
// 宣告一個Subscription例項。訂閱,監聽state變化來執行listener,都由例項來實現。
const subscription = new Subscription(store)
// 繫結監聽,當state變化時,通知訂閱者更新頁面
subscription.onStateChange = this.notifySubscribers
// 將store和subscription放入state中,稍後this.state將會作為context的value
this.state = {
store,
subscription
}
// 獲取當前的store中的state,作為上一次的state,將會在元件掛載完畢後,
// 與store新的state比較,不一致的話更新Provider元件
this.previousState = store.getState()
}
componentDidMount() {
this._isMounted = true
// 在元件掛載完畢後,訂閱更新。至於如何訂閱的,在下邊講到Subscription類的時候會講到,
// 這裡先理解為最開始的時候需要訂閱更新函式,便於在狀態變化的時候更新Provider元件
this.state.subscription.trySubscribe()
// 如果前後的store中的state有變化,那麼就去更新Provider元件
if (this.previousState !== this.props.store.getState()) {
this.state.subscription.notifyNestedSubs()
}
}
componentWillUnmount() {
// 元件解除安裝的時候,取消訂閱
if (this.unsubscribe) this.unsubscribe()
this.state.subscription.tryUnsubscribe()
this._isMounted = false
}
componentDidUpdate(prevProps) {
// 在元件更新的時候,檢查一下當前的store與之前的store是否一致,若不一致,說明應該根據新的資料做變化,
// 那麼依照原來的資料做出改變已經沒有意義了,所以會先取消訂閱,再重新宣告Subscription例項,
// 繫結監聽,設定state為新的資料
if (this.props.store !== prevProps.store) {
this.state.subscription.tryUnsubscribe()
const subscription = new Subscription(this.props.store)
subscription.onStateChange = this.notifySubscribers
this.setState({ store: this.props.store, subscription })
}
}
notifySubscribers() {
// notifyNestedSubs() 實際上會通知讓listener去執行,作用也就是更新UI
this.state.subscription.notifyNestedSubs()
}
render() {
const Context = this.props.context || ReactReduxContext
// 將this.state作為context的value傳遞下去
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
所以結合程式碼看這個問題:Provider是怎麼把store放入context中的,很好理解。
Provider最主要的功能是從props中獲取我們傳入的store,並將store作為context的其中一個值,向下層元件下發。
但是,一旦store變化,Provider要有所反應,以此保證將始終將最新的store放入context中。所以這裡要用訂閱來實現更新。自然引出Subscription類,通過該類的例項,將onStateChange監聽到一個可更新UI的事件this.notifySubscribers
上:
subscription.onStateChange =this.notifySubscribers
元件掛載完成後,去訂閱更新,至於這裡訂閱的是什麼,要看Subscription的實現。這裡先給出結論:本質上訂閱的是onStateChange
,實現訂閱的函式是:Subscription類之內的trySubscribe
this.state.subscription.trySubscribe()
再接著,如果前後的state不一樣,那麼就去通知訂閱者更新,onStateChange就會執行,Provider元件就會更新。走到更新完成(componentDidUpdate),
會去比較一下前後的store是否相同,如果不同,那麼用新的store作為context的值,並且取消訂閱,重新訂閱一個新的Subscription例項。保證用的資料都是最新的。
所以說了這麼多,其實這只是Provider元件的更新,而不是應用內部某個被connect的元件的更新機制。我猜想應該有一個原因是考慮到了Provider有可能被巢狀使用,所以會有這種在Provider更新之後取新資料並重新訂閱的做法,這樣才能保證每次傳給子元件的context是最新的。
Subscription
我們已經發現了,Provider元件是通過Subscription類中的方法來實現更新的,而過一會要講到的connect高階元件的更新,也是通過它來實現,可見Subscription是React-Redux實現訂閱更新的核心機制。
import { getBatch } from './batch'
const CLEARED = null
const nullListeners = { notify() {} }
function createListenerCollection() {
const batch = getBatch()
let current = []
let next = []
return {
clear() {
// 清空next和current
next = CLEARED
current = CLEARED
},
notify() {
// 將next賦值給current,並同時賦值給listeners,這裡的next、current、listeners其實就是訂閱的更新函式佇列
const listeners = (current = next)
// 批量執行listeners
batch(() => {
for (let i = 0; i < listeners.length; i++) {
// 執行更新函式,這是觸發UI更新的最根本的原理
listeners[i]()
}
})
},
get() {
return next
},
subscribe(listener) {
let isSubscribed = true
// 將current複製一份,並賦值給next,下邊再向next中push listener(更新頁面的函式)
if (next === current) next = current.slice()
next.push(listener)
return function unsubscribe() {
if (!isSubscribed || current === CLEARED) return
isSubscribed = false
// 最終返回一個取消訂閱的函式,用於在下一輪的時候清除沒用的listener
if (next === current) next = current.slice()
next.splice(next.indexOf(listener), 1)
}
}
}
}
export default class Subscription {
constructor(store, parentSub) {
// 獲取store,要通過store來實現訂閱
this.store = store
// 獲取來自父級的subscription例項,主要是在connect的時候可能會用到
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
addNestedSub(listener) {
this.trySubscribe()
// 因為這裡是被parentSub呼叫的,所以listener也會被訂閱到parentSub上,也就是從Provider中獲取的subscription
return this.listeners.subscribe(listener)
}
notifyNestedSubs() {
// 通知listeners去執行
this.listeners.notify()
}
handleChangeWrapper() {
if (this.onStateChange) {
// onStateChange會在外部的被例項化成subcription例項的時候,被賦值為不同的更新函式,被賦值的地方分別的Provider和connect中
// 由於剛剛被訂閱的函式就是handleChangeWrapper,而它也就相當於listener。所以當狀態變化的時候,listener執行,onStateChange會執行
this.onStateChange()
}
}
isSubscribed() {
return Boolean(this.unsubscribe)
}
trySubscribe() {
if (!this.unsubscribe) {
// parentSub實際上是subcription例項
// 這裡判斷的是this.unsubscribe被賦值後的值,本質上也就是判斷parentSub有沒有,順便再賦值給this.unsubscribe
// 如果parentSub沒傳,那麼使用store訂閱,否則,呼叫parentSub.addNestedSub,使用React-Redux自己的訂閱邏輯。具體會在程式碼下邊的解釋中說明
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
// 建立listener集合
this.listeners = createListenerCollection()
}
}
tryUnsubscribe() {
// 取消訂閱
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
Subscription就是將頁面的更新工作和狀態的變化聯絡起來,具體就是listener(觸發頁面更新的方法,在這裡就是handleChangeWrapper),通過trySubscribe方法,根據情況被分別訂閱到store或者Subscription內部。放入到listeners陣列,當state變化的時候,listeners迴圈執行每一個監聽器,觸發頁面更新。
說一下trySubscribe中根據不同情況判斷直接使用store訂閱,還是呼叫addNestedSub來實現內部訂閱的原因。因為可能在一個應用中存在多個store,這裡的判斷是為了讓不同的store訂閱自己的listener,互不干擾。
如何向元件中注入state和dispatch
將store從應用頂層注入後,該考慮如何向元件中注入state和dispatch了。
正常順序肯定是先拿到store,再以某種方式分別執行這兩個函式,將store中的state和dispatch,以及元件自身的props作為mapStateToProps和mapDispatchToProps的引數,傳進去,我們就可以在這兩個函式之內能拿到這些值。而它們的返回值,又會再注入到元件的props中。
說到這裡,就要引出一個概念:selector。最終注入到元件的props是selectorFactory函式生成的selector的返回值,所以也就是說,mapStateToProps和mapDispatchToProps本質上就是selector。
生成的過程是在connect的核心函式connectAdvanced中,這個時候可以拿到當前context中的store,進而用store傳入selectorFactory生成selector,其形式為
function selector(stateOrDispatch, ownProps) {
...
return props
}
通過形式可以看出:selector就相當於mapStateToProps或者mapDispatchToProps,selector的返回值將作為props注入到元件中。
從mapToProps到selector
標題的mapToProps泛指mapStateToProps, mapDispatchToProps, mergeProps
結合日常的使用可知,我們的元件在被connect包裹之後才能拿到state和dispatch,所以我們先帶著上邊的結論,單獨梳理selector的機制,先看connect的原始碼:
export function createConnect({
connectHOC = connectAdvanced, // connectAdvanced函式是connect的核心
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {}) {
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{...options} = {}
) {
// 將我們傳入的mapStateToProps, mapDispatchToProps, mergeProps都初始化一遍
const initMapStateToProps = match(mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
// 返回connectHOC函式的呼叫,connectHOC的內部是connect的核心
return connectHOC(selectorFactory, {
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
...
})
}
}
export default createConnect()
connect實際上是createConnect,createConnect也只是返回了一個connect函式,而connect函式返回了connectHOC的呼叫(也就是connectAdvanced的呼叫),再繼續,connectAdvanced的呼叫最終會返回一個wrapWithConnect高階元件,這個函式的引數是我們傳入的元件。所以才有了connect平常的用法:
connect(mapStateToProps, mapDispatchToProps)(Component)
大家應該注意到了connect函式內將mapStateToProps,mapDispatchToProps,mergeProps都初始化了一遍,為什麼要去初始化而不直接使用呢?帶著疑問,我們往下看。
初始化selector過程
先看程式碼,主要看initMapStateToProps 和 initMapDispatchToProps,看一下這段程式碼是什麼意思。
const initMapStateToProps = match(mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
mapStateToPropsFactories 和 mapDispatchToPropsFactories都是函式陣列,其中的每個函式都會接收一個引數,為mapStateToProps或者mapDispatchToProps。而match函式的作用就是迴圈函式陣列,mapStateToProps或者mapDispatchToProps作為每個函式的入參去執行,當此時的函式返回值不為假的時候,賦值給左側。看一下match函式:
function match(arg, factories, name) {
// 迴圈執行factories,這裡的factories也就是mapStateToProps和mapDisPatchToProps兩個檔案中暴露出來的處理函式陣列
for (let i = factories.length - 1; i >= 0; i--) {
// arg也就是mapStateToProps或者mapDispatchToProps
// 這裡相當於將陣列內的每個函式之星了一遍,並將我們的mapToProps函式作為引數傳進去
const result = factories[i](arg)
if (result) return result
}
}
match迴圈的是一個函式陣列,下面我們看一下這兩個陣列,分別是mapStateToPropsFactories 和 mapDispatchToPropsFactories:
(下邊原始碼中的whenMapStateToPropsIsFunction函式會放到後邊講解)
-
mapStateToPropsFactories
-
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' // 當mapStateToProps是函式的時候,呼叫wrapMapToPropsFunc export function whenMapStateToPropsIsFunction(mapStateToProps) { return typeof mapStateToProps === 'function' ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') : undefined } // 當mapStateToProps沒有傳的時候,呼叫wrapMapToPropsConstant export function whenMapStateToPropsIsMissing(mapStateToProps) { return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined } export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
實際上是讓whenMapStateToPropsIsFunction和whenMapStateToPropsIsMissing都去執行一次mapStateToProps,然後根據傳入的mapStateToProps的情況來選出有執行結果的函式賦值給initMapStateToProps。
單獨看一下whenMapStateToPropsIsMissing
export function wrapMapToPropsConstant(getConstant) { return function initConstantSelector(dispatch, options) { const constant = getConstant(dispatch, options) function constantSelector() { return constant } constantSelector.dependsOnOwnProps = false return constantSelector } }
wrapMapToPropsConstant返回了一個函式,接收的引數是我們傳入的() => ({}),函式內部呼叫了入參函式並賦值給一個常量放入了constantSelector中,
該常量實際上就是我們不傳mapStateToProps時候的生成的selector,這個selector返回的是空物件,所以不會接受任何來自store中的state。同時可以看到constantSelector.dependsOnOwnProps = false,表示返回值與connect高階元件接收到的props無關。
-
-
mapDispatchToPropsFactories
-
import { bindActionCreators } from '../../redux-src' import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps' export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) { return typeof mapDispatchToProps === 'function' ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps') : undefined } // 當不傳mapDispatchToProps時,預設向元件中注入dispatch export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { return !mapDispatchToProps ? wrapMapToPropsConstant(dispatch => ({ dispatch })) : undefined } // 當傳入的mapDispatchToProps是物件,利用bindActionCreators進行處理 詳見redux/bindActionCreators.js export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { return mapDispatchToProps && typeof mapDispatchToProps === 'object' ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch)) : undefined } export default [ whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsObject ]
沒有傳遞mapDispatchToProps的時候,會呼叫whenMapDispatchToPropsIsMissing,這個時候,constantSelector只會返回一個dispatch,所以只能在元件中接收到dispatch。
當傳入的mapDispatchToProps是物件的時候,也是呼叫wrapMapToPropsConstant,根據前邊的瞭解,這裡注入到元件中的屬性是
bindActionCreators(mapDispatchToProps, dispatch)的執行結果。
-
現在,讓我們看一下whenMapStateToPropsIsFunction這個函式。它是在mapDispatchToProps與mapStateToProps都是函式的時候呼叫的,實現也比較複雜。這裡只單用mapStateToProps來舉例說明。
再提醒一下:下邊的mapToProps指的是mapDispatchToProps或mapStateToProps
// 根據mapStateToProps函式的引數個數,判斷元件是否應該依賴於自己的props
export function getDependsOnOwnProps(mapToProps) {
return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
export function wrapMapToPropsFunc(mapToProps, methodName) {
// 最終wrapMapToPropsFunc返回的是一個proxy函式,返回的函式會在selectorFactory函式中
// 的finalPropsSelectorFactory內被呼叫並賦值給其他變數。
// 而這個proxy函式會在selectorFactory中執行,生成最終的selector
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
// 根據元件是否依賴自身的props決定呼叫的時候傳什麼引數
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
// 將proxy.mapToProps賦值為我們傳入的mapToProps
proxy.mapToProps = mapToProps
// 根據元件是否傳入了元件本身從父元件接收的props來確定是否需要向元件中注入ownProps,
// 最終會用來實現元件自身的props變化,也會呼叫mapToProps的效果
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
// 再去執行proxy,這時候proxy.mapToProps已經被賦值為我們傳進來的mapToProps函式,
// 所以props就會被賦值成傳進來的mapToProps的返回值
let props = proxy(stateOrDispatch, ownProps)
if (typeof props === 'function') {
// 如果返回值是函式,那麼再去執行這個函式,再將store中的state或dispatch,以及ownProps再傳進去
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(props, displayName, methodName)
return props
}
return proxy
}
}
wrapMapToPropsFunc返回的實際上是initProxySelector函式,initProxySelector的執行結果是一個代理proxy,可理解為將傳進來的資料(state或dispatch, ownProps)代理到我們傳進來的mapToProps函式。proxy的執行結果是proxy.mapToProps,本質就是selector。
頁面初始化執行的時候,dependsOnOwnProps為true,所以執行proxy.mapToProps(stateOrDispatch, ownProps),也就是detectFactoryAndVerify。在後續的執行過程中,會先將proxy的mapToProps賦值為我們傳入connect的mapStateToProps或者mapDispatchToProps,然後在依照實際情況元件是否應該依賴自己的props賦值給dependsOnOwnProps。(注意,這個變數會在selectorFactory函式中作為元件是否根據自己的props變化執行mapToProps函式的依據)。
總結一下,這個函式最本質上做的事情就是將我們傳入connect的mapToProps函式掛到proxy.mapToProps上,同時再往proxy上掛載一個dependsOnOwnProps來方便區分元件是否依賴自己的props。最後,proxy又被作為initProxySelector的返回值,所以初始化過程被賦值的initMapStateToProps、initMapDispatchToProps、initMergeProps實際上是initProxySelector的函式引用,它們執行之後是proxy,至於它們三個proxy是在哪執行來生成具體的selector的我們下邊會講到。
現在,回想一下我們的疑問,為什麼要去初始化那三個mapToProps函式?目的很明顯,就是準備出生成selector的函式,用來放到一個合適的時機來執行,同時決定selector要不要對ownProps的改變做反應。
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
建立selector,向元件注入props
準備好了生成selector的函式之後,就需要執行它,將它的返回值作為props注入到元件中了。先粗略的概括一下注入的過程:
- 取到store的state或dispatch,以及ownProps
- 執行selector
- 將執行的返回值注入到元件
下面我們需要從最後一步的注入開始倒推,來看selector是怎麼執行的。
注入的過程發生在connect的核心函式connectAdvanced之內,先忽略該函式內的其他過程,聚焦注入過程,簡單看下原始碼
export default function connectAdvanced(
selectorFactory,
{
getDisplayName = name => `ConnectAdvanced(${name})`,
methodName = 'connectAdvanced',
renderCountProp = undefined,
shouldHandleStateChanges = true,
storeKey = 'store',
withRef = false,
forwardRef = false,
context = ReactReduxContext,
...connectOptions
} = {}
) {
const Context = context
return function wrapWithConnect(WrappedComponent) {
// ...忽略了其他程式碼
// selectorFactoryOptions是包含了我們初始化的mapToProps的一系列引數
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
// pure表示只有當state或者ownProps變動的時候,重新計算生成selector。
const { pure } = connectOptions
/* createChildSelector 的呼叫形式:createChildSelector(store)(state, ownProps),
createChildSelector返回了selectorFactory的呼叫,而selectorFactory實際上是其內部根據options.pure返回的
impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory的呼叫,而這兩個函式需要的引數是
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
除了dispatch,其餘引數都可從selectorFactoryOptions中獲得。呼叫的返回值,就是selector。而selector需要的引數是
(state, ownprops)。所以得出結論,createChildSelector(store)就是selector
*/
function createChildSelector(store) {
// 這裡是selectorFactory.js中finalPropsSelectorFactory的呼叫(本質上也就是上面我們初始化的mapToProps的呼叫),傳入dispatch,和options
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
function ConnectFunction(props) {
const store = props.store || contextValue.store
// 僅當store變化的時候,建立selector
// 呼叫childPropsSelector => childPropsSelector(dispatch, options)
const childPropsSelector = useMemo(() => {
// 每當store變化的時候重新建立這個選擇器
return createChildSelector(store)
}, [store])
// actualChildProps就是最終要注入到元件中的props,也就是selector的返回值。
const actualChildProps = usePureOnlyMemo(() => {
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
const renderedWrappedComponent = useMemo(
// 這裡是將props注入到元件的地方
() => <WrappedComponent {...actualChildProps} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
}
// 最後return出去
return hoistStatics(Connect, WrappedComponent)
}
在注入過程中,有一個很重要的東西:selectorFactory。這個函式就是生成selector的很重要的一環。它起到一個上傳下達的作用,把接收到的dispatch,以及那三個mapToProps函式,傳入到selectorFactory內部的處理函式(pureFinalPropsSelectorFactory 或 impureFinalPropsSelectorFactory)中,selectorFactory的執行結果是內部處理函式的呼叫。而內部處理函式的執行結果就是將那三種selector(mapStateToProps,mapDispatchToProps,mergeProps)
執行後合併的結果。也就是最終要傳給元件的props
下面我們看一下selectorFactory的內部實現。為了清晰,只先一下內部的結構
// 直接將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
export function impureFinalPropsSelectorFactory(){}
export function pureFinalPropsSelectorFactory() {
// 整個過程首次初始化的時候呼叫
function handleFirstCall(firstState, firstOwnProps) {}
// 返回新的props
function handleNewPropsAndNewState() {
// 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
}
// 返回新的props
function handleNewProps() {
// 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
}
// 返回新的props
function handleNewState() {
// 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
}
// 後續的過程呼叫
function handleSubsequentCalls(nextState, nextOwnProps) {}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// 第一次渲染,呼叫handleFirstCall,之後的action派發行為會觸發handleSubsequentCalls
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
// finalPropsSelectorFactory函式是在connectAdvaced函式內呼叫的selectorFactory函式
export default function finalPropsSelectorFactory(
dispatch,
{ initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
const mapStateToProps = initMapStateToProps(dispatch, options)
// 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫,是改造
// 之後的mapStateToProps, 在下邊返回的函式內還會再呼叫一次
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// 根據是否傳入pure屬性,決定呼叫哪個生成selector的函式來計算傳給元件的props。並將匹配到的函式賦值給selectorFactory
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory // 當props或state變化的時候,才去重新計算props
: impureFinalPropsSelectorFactory // 直接重新計算props
// 返回selectorFactory的呼叫
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
可以看出來,selectorFactory內部會決定在什麼時候生成新的props。下面來看一下完整的原始碼
export function impureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch
) {
// 如果呼叫這個函式,直接將三個selector的執行結果合併返回
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(
mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
export function pureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
// 使用閉包儲存一個變數,標記是否是第一次執行
let hasRunAtLeastOnce = false
// 下邊這些變數用於快取計算結果
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
// 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫的函式內部的proxy函式的呼叫。
stateProps = mapStateToProps(state, ownProps)
/*
* 膝蓋已爛,太繞了
* 回顧一下proxy:
* const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {}
* return proxy
* */
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
// 返回計算後的props
return mergedProps
}
function handleNewPropsAndNewState() {
stateProps = mapStateToProps(state, ownProps)
// 由於這個函式的呼叫條件是ownProps和state都變化,所以有必要判斷一下dependsOnOwnProps
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewProps() {
// 判斷如果需要依賴元件自己的props,重新計算stateProps
if (mapStateToProps.dependsOnOwnProps) {
stateProps = mapStateToProps(state, ownProps)
}
// 同上
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// 將元件自己的props,dispatchProps,stateProps整合出來
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewState() {
const nextStateProps = mapStateToProps(state, ownProps)
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 由於handleNewState執行的大前提是pure為true,所以有必要判斷一下前後來自store的state是否變化
if (statePropsChanged)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleSubsequentCalls(nextState, nextOwnProps) {
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
// 依據不同的情況,呼叫不同的函式
if (propsChanged && stateChanged) return handleNewPropsAndNewState() // 當元件自己的props和注入的store中的某些state同時變化時,呼叫handleNewPropsAndNewState()獲取最新的props
if (propsChanged) return handleNewProps() // 僅當元件自己的props變化時,呼叫handleNewProps來獲取最新的props,此時的props包括注入的props,元件自身的props,和dpspatch內的函式
if (stateChanged) return handleNewState() // 僅當注入的store中的某些state變化時,呼叫handleNewState()獲取最新的props, 此時的props包括注入的props,元件自身的props,和dpspatch內的函式
// 如果都沒變化,直接返回先前快取的mergedProps,並且在以上三個函式中,都分別用閉包機制對資料做了快取
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// 第一次渲染,呼叫handleFirstCall,之後的action派發行為會觸發handleSubsequentCalls
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
export default function finalPropsSelectorFactory(
dispatch,
{ initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
const mapStateToProps = initMapStateToProps(dispatch, options) // 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫,是改造
// 之後的mapStateToProps, 在下邊返回的函式內還會再呼叫一次
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// 驗證mapToProps函式,有錯誤時給出提醒
if (process.env.NODE_ENV !== 'production') {
verifySubselectors(
mapStateToProps,
mapDispatchToProps,
mergeProps,
options.displayName
)
}
// 根據是否傳入了pure,決定計算新props的方式,預設為true
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
至此,我們搞明白了mapToProps函式是在什麼時候執行的。再來回顧一下這部分的問題:如何向元件中注入state和dispatch,讓我們從頭梳理一下:
傳入mapToProps
首先,在connect的時候傳入了mapStateToProps,mapDispatchToProps,mergeProps。再聯想一下用法,這些函式內部可以接收到state或dispatch,以及ownProps,它們的返回值會傳入元件的props。
基於mapToProps生成selector
需要根據ownProps決定是否要依據其變化重新計算這些函式的返回值,所以會以這些函式為基礎,生成代理函式(proxy),代理函式的執行結果就是selector,上邊掛載了dependsOnOwnProps屬性,所以在selectorFactory內真正執行的時候,才有何時才去重新計算的依據。
將selector的執行結果作為props傳入元件
這一步在connectAdvanced函式內,建立一個呼叫selectorFactory,將store以及初始化後的mapToProps函式和其他配置傳進去。selectorFactory內執行mapToProps(也就是selector),獲取返回值,最後將這些值傳入元件。
大功告成
React-Redux的更新機制
React-Redux的更新機制也是屬於訂閱釋出的模式。而且與Redux類似,一旦狀態發生變化,呼叫listener更新頁面。讓我們根據這個過程抓取關鍵點:
- 更新誰?
- 訂閱的更新函式是什麼?
- 如何判斷狀態變化?
不著急看程式碼,我覺得先用文字描述清楚這些關鍵問題,不再一頭霧水地看程式碼更容易讓大家理解。
更新誰?
回想一下平時使用React-Redux的時候,是不是隻有被connect過並且傳入了mapStateToProps的元件,會響應store的變化?
所以,被更新的是被connect過的元件,而connect返回的是connectAdvanced,並且並且connectAdvanced會返回我們傳入的元件,
所以本質上是connectAdvanced內部依據store的變化更新自身,進而達到更新真正元件的目的。
訂閱的更新函式是什麼?
這一點從connectAdvanced內部訂閱的時候可以很直觀地看出來:
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
訂閱的函式是checkForUpdates
,重要的是這個checkForUpdates做了什麼,能讓元件更新。在connectAdvanced中使用useReducer內建了一個reducer,這個函式做的事情就是在前置條件(狀態變化)成立的時候,dispatch一個action,來觸發更新。
如何判斷狀態變化?
這個問題很好理解,因為每次redux返回的都是一個新的state。直接判斷前後的state的引用是否相同,就可以了
connect核心--connectAdvanced
connectAdvanced是一個比較重量級的高階函式,上邊大致說了更新機制,但很多具體做法都是在connectAdvanced中實現的。原始碼很長,邏輯有一些複雜,我寫了詳細的註釋。看的過程需要思考函式之間的呼叫關係以及目的,每個變數的意義,帶著上邊的結論,相信不難看懂。
// 這是保留元件的靜態方法的庫
import hoistStatics from 'hoist-non-react-statics'
import React, {
useContext,
useMemo,
useEffect,
useLayoutEffect,
useRef,
useReducer
} from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'
import Subscription from '../utils/Subscription'
import { ReactReduxContext } from './Context'
const EMPTY_ARRAY = []
const NO_SUBSCRIPTION_ARRAY = [null, null]
// 內建的reducer
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
const initStateUpdates = () => [null, 0]
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
// 自己對於以上英文註釋的意譯:
// 當在服務端環境使用useLayoutEffect時候,react會發出警告,為了解決此問題,需要在服務端使用useEffect,瀏覽器端使用useLayoutEffect。
// useLayoutEffect會在所有的DOM變更之後同步呼叫傳入其中的回撥(effect),
// 所以在瀏覽器環境下需要使用它,因為connect將會在渲染被提交到DOM之後,再同步更新ref來儲存最新的props
// ReactHooks文件對useLayoutEffect的說明:在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。
// useEffect的effect將在每輪渲染結束後執行,useLayoutEffect的effect在dom變更之後,繪製之前執行。
// 這裡的effect做的是更新工作
// 在服務端渲染的時候頁面已經出來了,有可能js還未載入完成。
// 所以需要在SSR階段使用useEffect,保證在頁面由js接管後,如果需要更新了,再去更新。
// 而在瀏覽器環境則不存在這樣的問題
// 根據是否存在window確定是服務端還是瀏覽器端
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
export default function connectAdvanced(
selectorFactory,
// options object:
{
// 獲取被connect包裹之後的元件名
getDisplayName = name => `ConnectAdvanced(${name})`,
// 為了報錯資訊的顯示
methodName = 'connectAdvanced',
// 直接翻譯的英文註釋:如果被定義, 名為此值的屬性將新增到傳遞給被包裹元件的 props 中。它的值將是元件被渲染的次數,這對於跟蹤不必要的重新渲染非常有用。預設值: undefined
renderCountProp = undefined,
// connect元件是否應響應store的變化
shouldHandleStateChanges = true,
// 使用了多個store的時候才需要用這個,目的是為了區分該獲取哪個store
storeKey = 'store',
// 如果為 true,則將一個引用儲存到被包裹的元件例項中,
// 並通過 getWrappedInstance()獲取到。
withRef = false,
// 用於將ref傳遞進來
forwardRef = false,
// 元件內部使用的context,使用者可自定義
context = ReactReduxContext,
// 其餘的配置項,selectorFactory應該會用到
...connectOptions
} = {}
) {
//省略了一些報錯的邏輯
// 獲取context
const Context = context
return function wrapWithConnect(WrappedComponent) {
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
const displayName = getDisplayName(wrappedComponentName)
// 定義selectorFactoryOptions,為構造selector做準備
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
const { pure } = connectOptions
/* 呼叫createChildSelector => createChildSelector(store)(state, ownProps)
createChildSelector返回了selectorFactory的帶參呼叫,而selectorFactory實際上是其內部根據options.pure返回的
impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory的呼叫,而這兩個函式需要的引數是(state, ownProps)
*/
function createChildSelector(store) {
// 這裡是selectorFactory.js中finalPropsSelectorFactory的呼叫,傳入dispatch,和options
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// 根據是否是pure模式來決定是否需要對更新的方式做優化,pure在這裡的意義類似於React的PureComponent
const usePureOnlyMemo = pure ? useMemo : callback => callback()
function ConnectFunction(props) {
// props變化,獲取最新的context,forwardedRef以及元件其他props
const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
const { context, forwardedRef, ...wrapperProps } = props
return [context, forwardedRef, wrapperProps]
}, [props])
// propsContext或Context發生變化,決定使用哪個context,如果propsContext存在則優先使用
const ContextToUse = useMemo(() => {
// 使用者可能會用自定義的context來代替ReactReduxContext,快取住我們應該用哪個context例項
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
// Memoize the check that determines which context instance we should use.
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])
// 通過上層元件獲取上下文中的store
// 當上層元件最近的context變化的時候,返回該context的當前值,也就是store
const contextValue = useContext(ContextToUse)
// store必須存在於prop或者context中
// 判斷store是否是來自props中的store
const didStoreComeFromProps = Boolean(props.store)
// 判斷store是否是來自context中的store
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
// 從context中取出store,準備被selector處理之後注入到元件。優先使用props中的store
const store = props.store || contextValue.store
// 僅當store變化的時候,建立selector
// childPropsSelector呼叫方式: childPropsSelector(dispatch, options)
const childPropsSelector = useMemo(() => {
// selector的建立需要依賴於傳入store
// 每當store變化的時候重新建立這個selector
return createChildSelector(store)
}, [store])
const [subscription, notifyNestedSubs] = useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// 如果store是從props中來的,就不再傳入subscription例項,否則使用context中傳入的subscription例項
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// contextValue就是store,將store重新覆蓋一遍,注入subscription,這樣被connect的元件在context中可以拿到subscription
const overriddenContextValue = useMemo(() => {
if (didStoreComeFromProps) {
// 如果元件是直接訂閱到來自props中的store,就直接使用來自props中的context
return contextValue
}
// Otherwise, put this component's subscription instance into context, so that
// connected descendants won't update until after this component is done
// 意譯:
// 如果store是從context獲取的,那麼將subscription放入上下文,
// 為了保證在component更新完畢之前被connect的子元件不會更新
return {
...contextValue,
subscription
}
}, [didStoreComeFromProps, contextValue, subscription])
// 內建reducer,來使元件更新,在checkForUpdates函式中會用到,作為更新機制的核心
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
// Set up refs to coordinate values between the subscription effect and the render logic
/*
* 官方解釋:
* useRef 返回一個可變的 ref 物件,其 .current 屬性被初始化為傳入的引數(initialValue)。
* 返回的 ref 物件在元件的整個生命週期內保持不變。
*
* ref不僅用於DOM,useRef()的current屬性可以用來儲存值,類似於類的例項屬性
*
* */
const lastChildProps = useRef() // 元件的props,包括來自父級的,store,dispatch
const lastWrapperProps = useRef(wrapperProps) // 元件本身來自父元件的props
const childPropsFromStoreUpdate = useRef() // 標記來自store的props是否被更新了
const renderIsScheduled = useRef(false) // 標記更新的時機
/*
* actualChildProps是真正要注入到元件中的props
* */
const actualChildProps = usePureOnlyMemo(() => {
// Tricky logic here:
// - This render may have been triggered by a Redux store update that produced new child props
// - However, we may have gotten new wrapper props after that
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
// So, we'll use the child props from store update only if the wrapper props are the same as last time.
/*
* 意譯:
* 這個渲染將會在store的更新產生新的props時候被觸發,然而,我們可能會在這之後接收到來自父元件的新的props,如果有新的props,
* 並且來自父元件的props不變,我們應該依據新的child props來更新。但是來自父元件的props更新也會導致整體props的改變,不得不重新計算。
* 所以只在新的props改變並且來自父元件的props和上次一致(下邊程式碼中的判斷條件成立)的情況下,才去更新
*
* 也就是說只依賴於store變動引起的props更新來重新渲染
* */
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
/*
* 意譯:我們需要在每次重新渲染的時候同步執行這個effect。但是react將會在SSR的情況放下對於useLayoutEffect做出警告,
* 所以useIsomorphicLayoutEffect的最終結果是通過環境判斷得出的useEffect或useLayoutEffect。在服務端渲染的時候使用useEffect,
* 因為在這種情況下useEffect會等到js接管頁面以後再去執行,所以就不會warning了
* */
/*
* 整體看上下有兩個useIsomorphicLayoutEffect,不同之處在於它們兩個的執行時機。
*
* 第一個沒有傳入依賴項陣列,所以effect會在每次重新渲染的時候執行,負責每次重新渲染的
* 時候檢查來自store的資料有沒有變化,變化就通知listeners去更新
*
* 第二個依賴於store, subscription, childPropsSelector。所以在這三個變化的時候,去執行effect。
* 其內部的effect做的事情有別於第一個,負責定義更新函式checkForUpdates、訂閱更新函式,便於在第一個effect響應store更新的時候,
* 可以將更新函式作為listener執行,來達到更新頁面的目的
*
* */
useIsomorphicLayoutEffect(() => {
lastWrapperProps.current = wrapperProps // 獲取到元件自己的props
lastChildProps.current = actualChildProps // 獲取到注入到元件的props
renderIsScheduled.current = false // 表明已經過了渲染階段
// If the render was from a store update, clear out that reference and cascade the subscriber update
// 如果來自store的props更新了,那麼通知listeners去執行,也就是執行先前被訂閱的this.handleChangeWrapper(Subscription類中),
// handleChangeWrapper中呼叫的是onStateChange,也就是在下邊賦值的負責更新頁面的函式checkForUpdates
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
})
// Our re-subscribe logic only runs when the store/subscription setup changes
// 重新訂閱僅在store內的subscription變化時才會執行。這兩個變化了,也就意味著要重新訂閱,因為保證傳遞最新的資料,所以之前的訂閱已經沒有意義了
useIsomorphicLayoutEffect(() => {
// 如果沒有訂閱,直接return,shouldHandleStateChanges預設為true,所以預設情況會繼續執行
if (!shouldHandleStateChanges) return
// Capture values for checking if and when this component unmounts
// 當元件解除安裝的時候,用閉包,宣告兩個變數標記是否被取消訂閱和錯誤物件
let didUnsubscribe = false
let lastThrownError = null
// 當store或者subscription變化的時候,回撥會被重新執行,從而實現重新訂閱
const checkForUpdates = () => {
if (didUnsubscribe) {
// 如果取消訂閱了,那啥都不做
return
}
// 獲取到最新的state
const latestStoreState = store.getState()
let newChildProps, error
try {
// 使用selector獲取到最新的props
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}
if (!error) {
lastThrownError = null
}
// 如果props沒變化,只通知一下listeners更新
if (newChildProps === lastChildProps.current) {
/*
* 瀏覽器環境下,useLayoutEffect的執行時機是DOM變更之後,繪製之前。
* 由於上邊的useIsomorphicLayoutEffect在這個時機執行將renderIsScheduled.current設定為false,
* 所以會走到判斷內部,保證在正確的時機觸發更新
*
* */
if (!renderIsScheduled.current) {
notifyNestedSubs()
}
} else {
/*
* 如果props有變化,將新的props快取起來,並且將childPropsFromStoreUpdate.current設定為新的props,便於在第一個
* useIsomorphicLayoutEffect執行的時候能夠識別出props確實是更新了
* */
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// 當dispatch 內建的action時候,ConnectFunction這個元件會更新,從而達到更新元件的目的
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
latestStoreState,
error
}
})
}
}
// onStateChange的角色也就是listener。在provider中,賦值為更新listeners。在ConnectFunction中賦值為checkForUpdates
// 而checkForUpdates做的工作就是根據props的變化,相當於listener,更新ConnectFunction自身
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
// 第一次渲染後先執行一次,從store中同步資料
checkForUpdates()
// 返回一個取消訂閱的函式,目的是在元件解除安裝時取消訂閱
const unsubscribeWrapper = () => {
didUnsubscribe = true
subscription.tryUnsubscribe()
if (lastThrownError) {
throw lastThrownError
}
}
return unsubscribeWrapper
}, [store, subscription, childPropsSelector])
// 將元件的props注入到我們傳入的真實元件中
const renderedWrappedComponent = useMemo(
() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
/*
* 意譯:
如果這個元件訂閱了store的更新,就需要把它自己訂閱的例項往下傳,也就意味這其自身與其
後代元件都會渲染同一個Context例項,只不過可能會向context中放入不同的值
再套一層Provider,將被重寫的context放入value。
這是什麼意思呢?也就是說,有一個被connect的元件,又嵌套了一個被connect的元件,
保證這兩個從context中獲取的subscription是同一個,而它們可能都會往context中新增加值,
我加了一個,我的子元件也加了一個。最終的context是所有元件的value的整合,而subscription始終是同一個
* */
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
// 依賴於接收到的context,傳入的元件,context的value的變化來決定是否重新渲染
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
}
// 根據pure決定渲染邏輯
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
// 新增元件名
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
// 如果forwardRef為true,將ref注入到Connect元件,便於獲取到元件的DOM例項
if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef(
props,
ref
) {
return <Connect {...props} forwardedRef={ref} />
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
// 保留元件的靜態方法
return hoistStatics(Connect, WrappedComponent)
}
}
看完了原始碼,我們整體概括一下React-Redux中被connect的元件的更新機制:
這其中有三個要素必不可少:
- 根據誰變化(store)
- 更新函式(checkForUpdates)
- 將store和更新函式建立聯絡的Subscription
connectAdvanced函式內從context中獲取store,再獲取subscription例項(可能來自context或新建立),然後建立更新函式checkForUpdates,當元件初始化,或者store、Subscription例項、selector變化的時候,訂閱或者重新訂閱。在每次元件更新的時候,檢查一下store是否變化,有變化則通知更新,實際上執行checkForUpdates,本質上呼叫內建reducer更新元件。每次更新導致selector重新計算,所以元件總是能獲取到最新的props。所以說,更新機制的最底層是通過connectAdvanced內建的Reducer來實現的。