歡迎關注公衆號: 一口一個前端,不按期分享我所理解的前端知識前端
以前寫了一篇解讀Redux運行機制的文章,以後一直想再寫一篇React-Redux的解析,但這個源碼比較複雜,好在最近收穫了一些東西,分享出來。react
我在讀React-Redux源碼的過程當中,很天然的要去網上找一些參考文章,但發現這些文章基本都沒有講的很透徹,不少時候就是平鋪直敘把API挨個講一下,並且只講某一行代碼是作什麼的,卻沒有結合應用場景和用法解釋清楚爲何這麼作,加上源碼自己又很抽象,函數間的調用關係很是很差梳理清楚,最終結果就是越看越懵。我此次將嘗試換一種解讀方式,由最多見的用法入手,結合用法,提出問題,帶着問題看源碼裏是如何實現的,以此來和你們一塊兒逐漸梳理清楚React-Redux的運行機制。redux
文章用了一週多的時間寫完,粗看了一遍源碼以後,又邊看邊寫。源碼不算少,我儘可能把結構按照最容易理解的方式梳理,努力按照淺顯的方式將原理講出來, 但架不住代碼結構的複雜,不少地方依然須要花時間思考,捋清函數之間的調用關係並結合用法才能明白。文章有點長,能看到最後的都是真愛~數組
水平有限,不免有地方解釋的不到位或者有錯誤,也但願你們能幫忙指出來,不勝感激。瀏覽器
在這裏,我就默認你們已經會使用Redux了,它爲咱們的應用提供一個全局的對象(store)來管理狀態。 那麼如何將Redux應用在React中呢?想一下,咱們的最終目的是實現跨層級組件間通訊與狀態的統一管理。因此可使用Context這個特性。緩存
而這些都須要本身手動去作,React-Redux將上邊的都封裝起來了。讓咱們經過一段代碼看一下React-Redux的用法:bash
首先是在React的最外層應用上,包裹Provider,而Provider是React-Redux提供的組件,這裏作的事情至關於上邊的第一步antd
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將組件包裹起來:app
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作了什麼工做:
有了上邊的結論,但想必你們都比較好奇到底是怎麼實現的,上邊的幾項工做都是協同完成的,最終的表象體現爲下面幾個問題:
接下來,帶着這些問題來一條一條地分析源碼。
先從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變化時,通知訂閱者更新頁面,實際上也就是在connect過程當中被訂閱到react-redux的subscrption對象上的更新函數
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類的時候會講到,
// 這裏先理解爲最開始的時候須要訂閱更新函數,便於在狀態變化的時候執行更新函數
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組件就會執行下層組件訂閱到react-redux的更新函數。當Provider更新完成(componentDidUpdate),會去比較一下先後的store是否相同,若是不一樣,那麼用新的store做爲context的值,而且取消訂閱,從新訂閱一個新的Subscription實例。保證用的數據都是最新的。
我猜測應該有一個緣由是考慮到了Provider有可能被嵌套使用,因此會有這種在Provider更新以後取新數據並從新訂閱的作法,這樣才能保證每次傳給子組件的context是最新的。
Provider將執行觸發listeners執行的函數訂閱到了store。
咱們已經發現了,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訂閱,不然,調用context中獲取的subscrption來訂閱,保證都訂閱到一個地方,具體會在下邊說明
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來實現訂閱的緣由。由於前者的場景是Provider將listener訂閱到store中,此時的listeners數組內實際上是每一個connect內部的checkForUpdates函數(後邊會講到)。後者是connect內部將checkForUpdates放到listeners數組中,其實是利用Provider中傳過來的Subscrption實例來訂閱,保證全部被connect的組件都訂閱到一個Subscrption實例上。
將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泛指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都初始化了一遍,爲何要去初始化而不直接使用呢?帶着疑問,咱們往下看。
先看代碼,主要看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的改變作反應。
準備好了生成selector的函數以後,就須要執行它,將它的返回值做爲props注入到組件中了。先粗略的歸納一下注入的過程:
下面咱們須要從最後一步的注入開始倒推,來看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的更新機制也是屬於訂閱發佈的模式。並且與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的引用是否相同,就能夠了
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的組件的更新機制: 這其中有三個要素必不可少:
connectAdvanced函數內從context中獲取store
,再獲取subscription
實例(可能來自context或新建立),而後建立更新函數checkForUpdates
, 當組件初始化,或者store、Subscription實例、selector變化的時候,訂閱或者從新訂閱。在每次組件更新的時候,檢查一下store是否變化,有變化則通知更新, 實際上執行checkForUpdates,本質上調用內置reducer更新組件。每次更新致使selector從新計算,因此組件老是能獲取到最新的props。因此說,更新機制的最底層 是經過connectAdvanced內置的Reducer來實現的。
至此,圍繞經常使用的功能,React-Redux的源碼就解讀完了。回到文章最開始的三個問題:
如今咱們應該能夠明白,這三個問題對應着React-Redux的三個核心概念:
它們協同工做也就是React-Redux的運行機制:Provider將數據放入context,connect的時候會從context中取出store,獲取mapStateToProps,mapDispatchToProps,使用selectorFactory生成Selector做爲props注入組件。其次訂閱store的變化,每次更新組件會取到最新的props。
閱讀源碼最好的辦法是先肯定問題,有目的性的去讀。開始的時候我就是硬看,越看越懵,換了一種方式後收穫了很多,相信你也是。
歡迎關注個人公衆號: 一口一個前端,不按期分享我所理解的前端知識