歡迎關注公衆號: 一口一個前端,不按期分享我所理解的前端知識前端
以前寫了一篇解讀Redux運行機制的文章,以後一直想再寫一篇React-Redux的解析,但這個源碼比較複雜,好在最近收穫了一些東西,分享出來。react
我在讀React-Redux源碼的過程當中,很天然的要去網上找一些參考文章,但發現這些文章基本都沒有講的很透徹,不少時候就是平鋪直敘把API挨個講一下,並且只講某一行代碼是作什麼的,卻沒有結合應用場景和用法解釋清楚爲何這麼作,加上源碼自己又很抽象,函數間的調用關係很是很差梳理清楚,最終結果就是越看越懵。我此次將嘗試換一種解讀方式,由最多見的用法入手,結合用法,提出問題,帶着問題看源碼裏是如何實現的,以此來和你們一塊兒逐漸梳理清楚React-Redux的運行機制。redux
文章用了一週多的時間寫完,粗看了一遍源碼以後,又邊看邊寫。源碼不算少,我儘可能把結構按照最容易理解的方式梳理,努力按照淺顯的方式將原理講出來,但架不住代碼結構的複雜,不少地方依然須要花時間思考,捋清函數之間的調用關係並結合用法才能明白。文章有點長,能看到最後的都是真愛~segmentfault
水平有限,不免有地方解釋的不到位或者有錯誤,也但願你們能幫忙指出來,不勝感激。數組
在這裏,我就默認你們已經會使用Redux了,它爲咱們的應用提供一個全局的對象(store)來管理狀態。
那麼如何將Redux應用在React中呢?想一下,咱們的最終目的是實現跨層級組件間通訊與狀態的統一管理。因此可使用Context這個特性。瀏覽器
而這些都須要本身手動去作,React-Redux將上邊的都封裝起來了。讓咱們經過一段代碼看一下React-Redux的用法:緩存
首先是在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作了什麼工做:
提供connect高階組件,主要作了兩件事:
有了上邊的結論,但想必你們都比較好奇到底是怎麼實現的,上邊的幾項工做都是協同完成的,最終的表象體現爲下面幾個問題:
接下來,帶着這些問題來一條一條地分析源碼。
先從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。
閱讀源碼最好的辦法是先肯定問題,有目的性的去讀。開始的時候我就是硬看,越看越懵,換了一種方式後收穫了很多,相信你也是。
歡迎關注個人公衆號: 一口一個前端,不按期分享我所理解的前端知識