以前寫過一篇 Redux 的源碼解析文章,時隔幾個月我又看了看 React Redux 的源碼,這一次也是收穫滿滿,因此寫下了這篇博客記錄一下個人收穫。javascript
React Redux 不一樣於 Redux,Redux 的設計目的在於提供一個獨立於 UI 的數據中心,使得咱們能夠方便地在組件樹中的任意多個組件間共享數據;Redux 獨立於 React,能夠脫離 React 使用。而 React Redux 是爲了方便咱們將 Redux 與 React 結合使用,使得咱們能夠在 React 組件內方便地獲取 Store 中的數據而且訂閱 Store 內數據的變化;當 Store 內數據變化後,可以使得咱們相應的組件根據必定的條件從新渲染。html
因此 React Redux 的核心點在於:java
這篇文章能夠就這兩點圍繞展開解讀。react
在閱讀源碼以前,最好熟知如何使用 React-Redux,若是對於 API 還不熟悉的話,能夠閱讀官網的相關文檔。ios
另外在你的電腦上最好打開一份同版本的源代碼項目,以便跟隨閱讀。git
首先咱們下載 GitHub 上的 React Redux 源碼,我所閱讀的源碼版本是 7.1.3
,讀者也最好是下載該版本的源碼,以避免在閱讀時產生困惑。github
git clone https://github.com/reduxjs/react-redux.git
複製代碼
下載下來以後咱們就能夠看到源碼的文件結構了,src
目錄具體文件結構以下:redux
src
├── alternate-renderers.js
├── components
│ ├── Context.js
│ ├── Provider.js
│ └── connectAdvanced.js
├── connect
│ ├── connect.js
│ ├── mergeProps.js
│ ├── mapDispatchToProps.js
│ ├── mapStateToProps.js
│ ├── selectorFactory.js
│ ├── verifySubselectors.js
│ └── wrapMapToProps.js
├── hooks
│ ├── useDispatch.js
│ ├── useReduxContext.js
│ ├── useSelector.js
│ └── useStore.js
├── index.js
└── utils
├── Subscription.js
├── batch.js
├── isPlainObject.js
├── reactBatchedUpdates.js
├── reactBatchedUpdates.native.js
├── shallowEqual.js
├── useIsomorphicLayoutEffect.js
├── useIsomorphicLayoutEffect.native.js
├── verifyPlainObject.js
├── warning.js
└── wrapActionCreators.js
複製代碼
咱們先從入口文件 index.js 開始看起。api
該文件內容比較簡單:數組
import Provider from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import { ReactReduxContext } from './components/Context'
import connect from './connect/connect'
import { useDispatch, createDispatchHook } from './hooks/useDispatch'
import { useSelector, createSelectorHook } from './hooks/useSelector'
import { useStore, createStoreHook } from './hooks/useStore'
import { setBatch } from './utils/batch'
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
import shallowEqual from './utils/shallowEqual'
setBatch(batch)
export {
Provider,
connectAdvanced,
ReactReduxContext,
connect,
batch,
useDispatch,
createDispatchHook,
useSelector,
createSelectorHook,
useStore,
createStoreHook,
shallowEqual
}
複製代碼
主要就是從各個文件中導出咱們須要使用的 API。
咱們使用的最多的幾個 API 是 Provider
和 connect
,先從這兩個看起,其餘 API 放到後面看。
根據 React-Redux 官網的實例,咱們在使用的時候須要引入 Provider 組件,而後將其包裹在咱們的根組件外邊,傳入 store 數據:
它使得咱們的應用能夠方便地獲取 store 對象,那麼它是如何實現的呢?
咱們知道 React 有一個概念叫 Context,它一樣提供一個 Provider 組件,而且可使得 Consumer 能夠在內部的任意位置獲取 Provider 提供的數據,因此這二者有很是類似的功能和特性。React-Redux 就是基於 Context API 實現的,這樣頂層組件提供的 store 對象能夠在內部位置獲取到。
React-Redux 的 Provider 組件源碼以下:
function Provider({ store, context, children }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
const previousState = useMemo(() => store.getState(), [store])
useEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider> } 複製代碼
能夠看到這是一個簡單地函數組件,在開始的時候建立了一個 contextValue 對象,內部包含一個 store 和一個 subscription。這個 contextValue 會在最後做爲 value 傳入 Context API,即最後一行代碼。
這裏值得學習的是 contextValue 的計算方法,基於 useMemo Hook 實現了對於 contextValue 的緩存,只有當 store 發生變化的時候這個值纔會從新計算,減小了計算的開支。
這個 store 對象是咱們內部組件所須要的,那這個 subscription 對象是啥呢?這個 subscription 對象實際上是 React-Redux 實現數據訂閱的關鍵所在,咱們以後再關注這一塊,如今只須要知道這是很重要的一個內容就行。
關於 Context 的來源,能夠看到是 Provider 接受到的 context props 或者內部默認的 ReactReduxContext。因此咱們知道了咱們能夠提供一個默認的 context 組件給 Provider 來實現進一步的封裝。這裏多數狀況下咱們都是使用的 ReactReduxContext:
export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)
if (process.env.NODE_ENV !== 'production') {
// 在非生產環境中能夠在 devtools 中看到該組件名稱
ReactReduxContext.displayName = 'ReactRedux'
}
複製代碼
能夠看到這是經過 React.createContext API 建立的一個 context 對象,平平無奇。
那麼咱們經過 Context.Provider 提供了咱們的 contextValue 給下層組件,那麼咱們的下層組件是如何獲取咱們的 contextValue 的呢?
這個時候咱們就應該想到咱們的 connect 函數,確定是它內部完成了這些工做,下面咱們看看 connect 函數作了什麼。
connect 函數來源於 connect.js 的 createConnect 函數調用,這是一個高階函數,返回了咱們真正使用到的 connect API:
// createConnect with default args builds the 'official' connect behavior. Calling it with
// different options opens up some testing and extensibility scenarios
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
// 這是咱們真正使用到的 connect 函數
return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) {
// match 函數的第二個參數是一個函數數組,經過將第一個參數做爲調用參數,依次順序調用第二個參數內的函數來獲取結果
// 返回的函數就是通過包裝後的對應函數
const initMapStateToProps = match(
mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps'
)
const initMapDispatchToProps = match(
mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps'
)
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
return connectHOC(selectorFactory, {
// used in error messages
methodName: 'connect',
// used to compute Connect's displayName from the wrapped component's displayName.
getDisplayName: name => `Connect(${name})`,
// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
shouldHandleStateChanges: Boolean(mapStateToProps),
// passed through to selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
// any extra options args can override defaults of connect or connectAdvanced
...extraOptions
})
}
}
複製代碼
爲何會有這麼個高階函數來產生咱們所使用的 connect 函數呢?第一段註釋說得很清楚了,由於給 createConnect 函數傳入不一樣的參數能夠生成不一樣的 connect 函數,用於咱們的測試或者其餘場景,在計算咱們真正使用的 connect 函數時,使用到的所有都是默認參數。
createConnect 函數返回了咱們真正使用到的 connect 函數,這個函數所接受的參數咱們就應該比較熟悉了。若是還不熟悉的話,能夠參考 React-Redux 官方文檔。
connect 函數在接受到咱們傳入的參數後,會執行三次 match 函數來計算初始化 mapDispatchToProps
、mapStateToProps
和 mergeProps
的函數。咱們看看 match 函數是如何定義的:
/* connect is a facade over connectAdvanced. It turns its args into a compatible selectorFactory, which has the signature: (dispatch, options) => (nextState, nextOwnProps) => nextFinalProps connect passes its args to connectAdvanced as options, which will in turn pass them to selectorFactory each time a Connect component instance is instantiated or hot reloaded. selectorFactory returns a final props selector from its mapStateToProps, mapStateToPropsFactories, mapDispatchToProps, mapDispatchToPropsFactories, mergeProps, mergePropsFactories, and pure args. The resulting final props selector is called by the Connect component instance whenever it receives new props or store state. */
function match(arg, factories, name) {
for (let i = factories.length - 1; i >= 0; i--) {
const result = factories[i](arg)
if (result) return result
}
return (dispatch, options) => {
throw new Error(
`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${ options.wrappedComponentName }.`
)
}
}
複製代碼
match 函數的第一個參數是咱們傳入的原始的 mapStateToProps
、mapDispatchToProps
或者 mergeProps
函數。第二個參數是 connect 函數傳入的一個數組,第三個參數是指定的函數名稱。
在 match 函數內,會遍歷第二個數組參數,依次執行數組中的函數,而且將咱們原始的 mapxxxToProps
或者 mergeProps
函數做爲參數傳入這個參數,若是返回的結果爲非 False 值,就直接返回,做爲咱們的 init..
函數。
若是數組遍歷完成後仍是沒有獲得非 False 的返回值,那麼就返回一個__標準格式__ (注意返回函數的格式)的報錯函數,說明用戶傳入的 map..
函數不符合要求,看起來 match 函數會對咱們傳入的參數作一次校驗。
那麼上一段中提到的標準格式是什麼格式呢?咱們能夠看一下函數定義上面的大段註釋。這段註釋說明 connect 函數只是 connectAdvanced 函數的一個代理人,它所作的工做就是將咱們傳入的參數轉化爲能夠供 selectorFactory 使用的參數。而能夠供 selectorFactory 使用的一個標準就是參數的結構符合以下定義:
(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps
複製代碼
這個結構就是咱們的標準格式。
被轉化爲這個格式的 init
系列函數會被做爲參數傳入 connectAdvanced 函數,而 connectAdvanced 函數會根據傳入的參數和 store 對象計算出最終組件須要的 props。
這個標準格式很是重要,由於後面的不少地方的代碼都跟這個有關,因此咱們須要注意一下。
咱們已經知道了 match 函數的做用,因此咱們接下來看一下 match 是如何經過第二個參數來計算咱們的標準化後的 mapStateToProps
……等函數的。
這個函數是根據下面代碼計算出來的:
const initMapStateToProps = match(
mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps'
)
複製代碼
match 的原理咱們已經明白了,因此標準化的關鍵就在於 mapStateToPropsFactories 函數數組。
咱們如今看一下這個數組:
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapStateToPropsIsFunction(mapStateToProps) {
return typeof mapStateToProps === 'function'
? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
: undefined
}
export function whenMapStateToPropsIsMissing(mapStateToProps) {
return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
}
export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
複製代碼
能夠看到這個函數數組一共就有兩個函數,會一次傳入咱們的 mapStateToProps
參數進行計算,獲得告終果就會返回。
第一個函數 whenMapStateToPropsIsFunction
是計算當咱們的 mapStateToProps
參數爲函數時的結果,第二個函數時計算當咱們傳入的 mapStateToProps
是一個 False 值時的默認結果(即若是咱們的 mapStateToProps
爲 null 時,selectorFactory 函數應該使用的函數)。
咱們再深刻看看 wrapMapToPropsFunc
和 wrapMapToPropsConstant
函數,其中wrapMapToPropsFunc
函數是重點。
wrapMapToPropsFunc
函數代碼以下:
// Used by whenMapStateToPropsIsFunction and whenMapDispatchToPropsIsFunction,
// this function wraps mapToProps in a proxy function which does several things:
//
// * Detects whether the mapToProps function being called depends on props, which
// is used by selectorFactory to decide if it should reinvoke on props changes.
//
// * On first call, handles mapToProps if returns another function, and treats that
// new function as the true mapToProps for subsequent calls.
//
// * On first call, verifies the first result is a plain object, in order to warn
// the developer that their mapToProps function is not returning a valid result.
//
export function wrapMapToPropsFunc(mapToProps, methodName) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
// allow detectFactoryAndVerify to get ownProps
// 第一次運行時將 dependsOnOwnProps 設置成 true
// 使得 detectFactoryAndVerify 在運行的時候能夠得到第二個參數,等第二次運行時
// proxy.mapToProps 和 denpendsOnOwnProps 都是通過計算獲得的
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify( stateOrDispatch, ownProps ) {
proxy.mapToProps = mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
let props = proxy(stateOrDispatch, ownProps)
if (typeof props === 'function') {
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
}
}
複製代碼
能夠看到該函數返回的結果確實符合咱們的標準格式,而且 proxy.mapToProps
返回的結果即爲標準格式中的最終結果:nextFinalProps
。
能夠看到返回的 initProxySelector
函數能夠接受 dispatch 函數和 options 參數,返回一個 proxy 函數對象,這個函數接受 stateOrDispatch 函數和 ownProps 參數,最終返回一個計算結果。
這裏的關鍵在於 proxy.dependsOnOwnProps
屬性,這個屬性決定了咱們在調用 proxy.mapToProps
函數時是否傳入第二個函數。那這個 dependsOnOwnProps
屬性是如何獲得的呢?
繼續看代碼,能夠發如今第一次執行 initProxySelector
函數的時候,默認 dependsOnOwnProps
參數爲 true,這是爲了讓 detectFactoryAndVerify
函數執行時能夠獲得 ownProps 這個參數。而detectFactoryAndVerify
的存在是爲了在第一次運行 mapToProps
函數時進行一些額外的工做,好比計算dependsOnOwnProps
屬性、校驗返回的 props 結果。在 detectFactoryAndVerify
函數內部會從新爲 proxy.mapToProps
賦值,這意味着第二次運行 proxy.mapToProps
函數的時候,就不會從新計算那些參數了。
另外若是咱們的 mapStateToProps
返回的結果是一個函數,則在後續的計算中,這個返回的函數會做爲真正的 mapToProps
函數進行 props 的計算。這也是爲何官方文檔中會有以下這段話:
You may define
mapStateToProps
andmapDispatchToProps
as a factory function, i.e., you return a function instead of an object. In this case your returned function will be treated as the realmapStateToProps
ormapDispatchToProps
, and be called in subsequent calls. You may see notes on Factory Functions or our guide on performance optimizations.
也就是除了能夠返回一個純對象之外,還能夠返回一個函數。
再來看一下 getDependsOnOwnProps
函數是如何計算 dependsOnOwnProps
屬性的:
// 該函數用於計算 mapToProps 是否須要使用到 props
// 依據是 function.length,若是 function.length === 1 就說明只須要 stateOrDispatch
// 若是 function.length !== 1,說明就須要 props 進行計算。
export function getDependsOnOwnProps(mapToProps) {
return mapToProps.dependsOnOwnProps !== null &&
mapToProps.dependsOnOwnProps !== undefined
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
複製代碼
當 mapToProps.dependsOnOwnProps
有值時就直接使用這個值做爲結果,再也不從新計算;若是仍是沒有值的話, 須要進行一次計算,計算的邏輯就是 mapToProps
函數的參數格式,即咱們傳遞給 connect
函數的 mapStateToProps
函數的參數個數,只有當參數個數爲 1 的時候纔不會傳入 ownProps
參數。
關於 wrapMapToPropsConstant
函數,這是用來計算當咱們傳入的 mapStateToProps
函數爲 null 時的結果的函數。代碼以下:
export function wrapMapToPropsConstant(getConstant) {
return function initConstantSelector(dispatch, options) {
const constant = getConstant(dispatch, options)
function constantSelector() {
return constant
}
constantSelector.dependsOnOwnProps = false
return constantSelector
}
}
複製代碼
能夠看到也是對結果進行了一下標準化,而後計算獲得的常量 constant
,返回 constantSelector
做爲結果,其 dependsOnOwnProps
屬性爲 fasle
,由於咱們沒有傳入對應參數,也就沒有依賴 ownProps 了。最終獲得的結果就是一個 undefined
對象,由於這種狀況下,getConstant
函數爲一個空函數: () => {}
。
計算該函數用到的函數數組爲:
import { bindActionCreators } from 'redux'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
return typeof mapDispatchToProps === 'function'
? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
: undefined
}
export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
return !mapDispatchToProps
? wrapMapToPropsConstant(dispatch => ({ dispatch }))
: undefined
}
export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
return mapDispatchToProps && typeof mapDispatchToProps === 'object'
? wrapMapToPropsConstant(dispatch =>
bindActionCreators(mapDispatchToProps, dispatch)
)
: undefined
}
export default [
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject
]
複製代碼
這裏會有三個函數,分別用於處理傳入的 mapDispatchToProps
是函數、null 和對象時的狀況。
當傳入的 mapDispatchToProps
爲函數時,一樣也是調用 wrapMapToPropsFunc
計算結果,這個與 initMapStateToProps
的計算邏輯一致。
當傳入的 mapDispatchToProps
爲 null 時,處理的邏輯同 initMapStateToProps
,區別在於傳入的參數不是空函數,而是一個返回對象的函數,對象默認包含 dispatch
函數,這就使得咱們使用 React-Redux 之後,能夠在內部經過 this.props.dispatch
訪問到 store 的 dispatch API:
Once you have connected your component in this way, your component receives
props.dispatch
. You may use it to dispatch actions to the store.
當傳入的 mapDispatchToProps
爲對象時,說明這是一個 ActionCreator 對象,能夠經過使用 redux 的 bindActionCreator
API 將這個 ActionCreator 轉化爲包含不少函數的對象並 merge 到 props。
這一個函數的計算比較簡單,代碼以下:
import verifyPlainObject from '../utils/verifyPlainObject'
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
export function wrapMergePropsFunc(mergeProps) {
return function initMergePropsProxy( dispatch, { displayName, pure, areMergedPropsEqual } ) {
let hasRunOnce = false
let mergedProps
return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)
if (hasRunOnce) {
// 這裏若是 pure === true 而且新舊 props 內容未變的時候
// 就不對 mergedProps 進行賦值,這樣能夠確保原來內容的引用不變,
// 可讓咱們的 useMemo 或者 React.memo 起做用。
if (!pure || !areMergedPropsEqual(nextMergedProps, mergedProps))
mergedProps = nextMergedProps
} else {
hasRunOnce = true
mergedProps = nextMergedProps
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(mergedProps, displayName, 'mergeProps')
}
return mergedProps
}
}
}
export function whenMergePropsIsFunction(mergeProps) {
return typeof mergeProps === 'function'
? wrapMergePropsFunc(mergeProps)
: undefined
}
export function whenMergePropsIsOmitted(mergeProps) {
return !mergeProps ? () => defaultMergeProps : undefined
}
export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
複製代碼
基本上就是直接對 stateProps
、dispatchProps
和 ownProps
三者的合併,加上了一些基本的校驗。
如今咱們獲得了三個主要的函數:initMapStateToProps
、initMapDispatchToProps
和 initMergeProps
。咱們知道了 React-Redux 是如何經過咱們傳入的參數結合 store 計算出被 connect 的組件的 props 的。
下面咱們再來進一步瞭解一下,selectorFactory 函數是如何基於咱們的 init...
系列函數計算最終的 props 的。
找到文件中的 selectorFactory.js
文件,能夠看到 finalPropsSelectorFactory
函數,這個就是咱們的 selectorFactory
函數。
代碼以下:
export default function finalPropsSelectorFactory( dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) {
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
if (process.env.NODE_ENV !== 'production') {
verifySubselectors(
mapStateToProps,
mapDispatchToProps,
mergeProps,
options.displayName
)
}
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
複製代碼
能夠看到一開始經過 init...
系列函數計算出了須要的 mapStateToProps
、mapDispatchToProps
和 mergeProps
函數。
隨後根據 options.pure
的值選擇不一樣的函數進行下一步計算。
當 options.pure === true
時,意味着咱們的組件爲」純組件「。
React Redux 源碼不得不提的一個點就是配置項中的 pure 參數,咱們能夠在 createStore 的時候傳入該配置,該配置默認爲 true。
當 pure 爲 true 的時候,React Redux 內部有幾處地方就會針對性地進行優化,好比咱們這裏看到的 selectFactory。當 pure 爲不一樣的值時選擇不一樣的函數進行 props 的計算。若是咱們的 pure 爲 false,則每次都進行相應計算產生新的 props,傳遞給咱們的內部組件,觸發 rerender。
當咱們的 pure 爲 true 的時候,React Redux 會緩存上一次計算的相應結果,而後在下一次計算後對比結果是否相同,若是相同的話就會返回上一次的計算結果,一旦咱們的組件是純組件,則傳入相同的 props 不會致使組件 rerender,達到了性能優化的目的。
當 pure 爲 false 時,調用 impureFinalPropsSelectorFactory 計算 props:
export function impureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch ) {
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(
mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
複製代碼
這樣每次計算都會返回新的 props,致使組件一直 rerender。
當 pure 爲 true 時,調用 pureFinalPropsSelectorFactory 計算 props:
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
stateProps = mapStateToProps(state, ownProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
function handleNewPropsAndNewState() {
stateProps = mapStateToProps(state, ownProps)
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewProps() {
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewState() {
const nextStateProps = mapStateToProps(state, ownProps)
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
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()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
複製代碼
能夠看到第一次計算時的過程跟 impureFinalPropsSelectorFactory 一致,可是多了個閉包內緩存的過程,在隨後的 props 計算當中,會根據 state 和 props 的變化狀況選擇不一樣的函數進行計算,這樣作是爲了儘量的減小計算量,優化性能。若是 state 和 props 都沒有發生變化的話,就直接返回緩存的 props。
能夠看到這段代碼裏面對比變量是否不一樣的函數有這麼幾個:areOwnPropsEqual
、areStatesEqual
、areStatePropsEqual
。在前文中咱們還看到過 areMergedPropsEqual
這個函數,他們都在 connect 函數定義時已經被賦值:
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual
複製代碼
strictEqual
的定義:
function strictEqual(a, b) {
return a === b
}
複製代碼
而 shallowEqual
的定義以下:
// 眼尖的朋友可能會發現這段代碼來自於 React 源碼
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false
}
}
return true
}
複製代碼
能夠看到前者的對比簡單粗暴,後者的對比更加細膩。
爲何 state 的對比會跟其餘三者不同呢?
這是由於 state 比較特殊,查看 Redux 的源碼:combineReducers.ts。不難發現當咱們的全部 reducers 返回的內容不變(維持原有的引用)時,咱們最終獲得的 state(Store.getState()
返回的對象)也會維持原有引用,使得 oldState === newState
成立,因此咱們在 React Redux 中對於 state 的對比會比其餘三個要簡單許多。
爲何 Redux 可以確保 reducer 沒有修改 state 的時候返回的是原來的 state,而 reducer 修改後的 state 的引用關係必定發生了變化呢?是由於 redux 要求使用者在定義 reducer 時按照這樣的要求作,在 reducer 產生新數據時必定要新建對象(經過擴展語法...
或者 Object.assisgn
),在沒有匹配到 action.type 時必定返回舊對象。這一點能夠在以前提到的 combineReducers 的源碼中仍然能夠看到許多針對性的檢查。
看完 2.4 小節,咱們其實能夠發現 selectorFactory 確實符合第二大節開始時提到的標準格式:
(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps
複製代碼
我但願你們可以記住這個結論,由於有助於咱們後面理解相關的代碼。
瞭解了 selectorFactory 的工做原理以後,咱們再看看在 connectAdvanced 內部是如何使用它的。
咱們已經知曉了是 React Redux 如何經過 state 和 props 計算出下一次渲染所須要使用的 props。這一節回到 connectAdvenced 函數看看咱們的 props 是在什麼時機被計算的。
connectAdvanced 函數實際上就是咱們使用到的 connect 函數,它接受相應配置以及相關組件,返回給咱們一個高階組件。
打開 src/components/connectAdvanced.js
文件,能夠看到該函數在前面一小部分作了部分校驗以後,直接返回了一個擁有一大段代碼的函數:wrapWithConnect
,這個函數大概有三百多行,它就是咱們執行 connect(...)
以後返回的函數,可想而知該函數接受一個__咱們定義的 UI 組件,返回給咱們一個容器組件__。
咱們依次看下去這段代碼,揀一部分最重要的代碼進行分析。
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
const { pure } = connectOptions
function createChildSelector(store) {
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
複製代碼
首先能夠看到這段代碼,它根據相關參數構建了一個 selectorFactoryOptions
對象,而後新建了一個 createChildSelector
函數,這個函數用於調用咱們以前分析過的 selectorFactory 函數,咱們知道 selectorFactory 函數符合咱們的標準格式,因此調用 selectorFactory 會獲得一個新的函數,該函數接受 state 和 props,返回計算出的下一次渲染所須要的 props。因此 createChildSelector 獲得的結果就是一個 props 計算函數。這裏之因此要這麼作是爲了計算獲得當前 store 須要用到的 props 計算函數,防止後面須要計算時又要從新調用。而當咱們的 store 對象發生變化之後,這個函數又會被從新調用:
// childPropsSelector 函數用於根據 mapStateToProps、mapDispatchToProps 等配置
// 計算 store 更新後的組件 props
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return createChildSelector(store)
}, [store])
複製代碼
能夠看到這裏也使用 useMemo 進行了優化。
昨晚一些前置工做以後,內部定義了一個 ConnectFunction
,這就是咱們真正用於渲染的組件,最後會被 connect()()
返回。咱們向容器組件傳遞 props 的時候,就是傳遞給了 ConnectFunction。
在一開始,ConnectFunction 會準備一些將要用到的數據:
const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
// Distinguish between actual "data" props that were passed to the wrapper component,
// and values needed to control behavior (forwarded refs, alternate context instances).
// To maintain the wrapperProps object reference, memoize this destructuring.
const { forwardedRef, ...wrapperProps } = props
return [props.context, forwardedRef, wrapperProps]
}, [props])
// 根據 propsContext 和 Context 計算要使用的 context,其中 propsContext 來自於外部傳遞,Context 來自於內部
// 若是 ContainerComponent 有接受到 context 屬性,那麼就是用傳入的這個 context,不然使用 Context.js 定義的那個。
// 同時那也是提供 store 對象的那個 context。
const ContextToUse = useMemo(() => {
// 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]) // 經過 useContext 使用 context,而且訂閱其變化,當 context 變化時會 re-render。 // Retrieve the store and ancestor subscription via context, if available const contextValue = useContext(ContextToUse) // The store _must_ exist as either a prop or in context. // We'll check to see if it _looks_ like a Redux store first. // This allows us to pass through a `store` prop that is just a plain value. // 檢查 props 以及 context 中是否有 store,若是都沒有那就無法玩了。 // 因此這裏咱們其實也能夠給 ContainerComponent 傳入一個 store props 做爲咱們 connect 的 store const didStoreComeFromProps = Boolean(props.store) && Boolean(props.store.getState) && Boolean(props.store.dispatch) const didStoreComeFromContext = Boolean(contextValue) && Boolean(contextValue.store) // Based on the previous check, one of these must be true // 使用傳入的 store,若是 ContainerComponent 接收到了 store,那就用它做爲 store。 // 實驗證實確實可使用 DisplayComponent 的 props 傳入 store,而且若是傳入了的話,該組件就會使用 props 中的 store 而非 context 中的。 const store = didStoreComeFromProps ? props.store : contextValue.store 複製代碼
上面代碼中的 props 就是咱們傳遞給容器組件的 props,首先會從其中解析出咱們的 forwardedRef、context 和 其餘 props 屬性。
forwardedRef 用於將咱們在容器組件上設置的 ref 屬性經過 React.forwardRef
API 轉交給內部的 UI 組件。
context 屬性用於計算咱們將要使用到的 context 。其餘 props 用於計算 UI 組件須要用到的 props。
當決定了要使用哪一個 context 的時候,就會經過 useContext API 使用其傳遞的值,因此咱們這裏用到的是 React Hooks,咱們經過 useContext API 便可使用到 Provider 內部的內容,而無需使用 Consumer 組件。
上面的最後一段代碼用於判斷咱們的 store 應該用哪一個來源的數據,能夠看到若是咱們給咱們的容器組件傳遞了 store 屬性的話,React Redux 就會使用這個 store 做爲數據來源,而不是頂層 Context 內的 store 對象。
若是咱們先不考慮組件是如何訂閱 store 更新的話,咱們能夠先看 UI 組件須要的 props 是如何計算出來而且應用的。
// childPropsSelector 函數用於根據 mapStateToProps、mapDispatchToProps 等配置
// 計算 store 更新後的組件 props
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return createChildSelector(store)
}, [store])
複製代碼
這段代碼咱們以前有分析過,createChildSelector
函數用於調用 selectorFactory,返回 selectorFactory 調用一次以後的結果,因爲 selectorFactory 符合咱們的設計規範:
(dispatch, options) => (nextState, nextOwnProps) => nextFinalProps
複製代碼
因此 childPropsSelector 返回的函數就符合下面的規範:
(nextState, nextOwnProps) => nextFinalProps
複製代碼
在整個函數的後半段,會有下面這段計算代碼:
// If we aren't running in "pure" mode, we don't want to memoize values.
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
// that just executes the given callback immediately.
const usePureOnlyMemo = pure ? useMemo : callback => callback()
// 最終使用的 props
const actualChildProps = usePureOnlyMemo(() => {
// ...忽略部分代碼
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
複製代碼
能夠看到這裏也根據 options.pure 選項決定是否緩存計算結果,若是不是 true 的話會每次更新 store、previousStateUpdateResult 或者 wrapperProps 都會致使 actualChildProps 從新計算。
因此這裏的 actualChildProps 就是咱們上方規範中的 nextFinalProps。
計算出最終用到的 props 以後,就開始渲染咱們的 UI 組件了:
// Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
複製代碼
上方代碼中的 WrappedComponent 組件即爲咱們傳入的 UI 組件,能夠看到最後 forwardedRef(原本是容器組件的 ref)最終被指到了內部的 UI 組件上。
renderedWrappedComponent 組件就是咱們渲染 UI 組件的結果,然而咱們還不能直接拿去返回給用戶渲染,咱們還要考慮其餘狀況,咱們接下來看看 UI 組件是如何訂閱 store 更新的。
在咱們的閱讀源碼的時候,可能常常會看到這個 subscription 對象,這個對象用於實現組件對於 store 更新的訂閱,是 React Redux 實現數據更新的關鍵。接下來咱們深刻該 API 的實現及功能。
打開咱們的 src/utils/Subscription.js
文件,該文件總共就兩個函數:createListenerCollection
和 Subscription
。前者是輔助工具,後者是咱們的真正使用到的 API。
先看這個工具函數:
const CLEARED = null
const nullListeners = { notify() {} }
function createListenerCollection() {
const batch = getBatch()
// the current/next pattern is copied from redux's createStore code.
// TODO: refactor+expose that code to be reusable here?
// 此處使用兩個隊列,防止在 notify 的同時進行 subscribe 致使的邊緣行爲
// Reference: https://github.com/reduxjs/react-redux/pull/1450#issuecomment-550382242
let current = []
let next = []
return {
clear() {
next = CLEARED
current = CLEARED
},
notify() {
const listeners = (current = next)
batch(() => {
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
})
},
get() {
return next
},
subscribe(listener) {
let isSubscribed = true
if (next === current) next = current.slice()
next.push(listener)
return function unsubscribe() {
if (!isSubscribed || current === CLEARED) return
isSubscribed = false
if (next === current) next = current.slice()
next.splice(next.indexOf(listener), 1)
}
}
}
}
複製代碼
能夠看到在這個函數返回的對象內部定義了兩個隊列,一個 next,一個 current,他們用於存放訂閱當前對象更新的 listener,一個用於存放下一步更新後的隊列。這樣作是爲了防止在 notify 執行後,隊列被遍歷時又開始調用 subscribe
或者 unsubscribe
函數致使隊列發生變化致使的一些邊緣問題。每次 notify 以前都會同步 current 爲 next,隨後的 subscribe 執行都是在 next 的基礎上執行。
總而言之這是一個返回純對象的函數,而這個對象的做用就是一個事件發佈訂閱中心,這是屬於觀察者模式的應用,咱們的全部 listener 都會監聽當前對象,一旦當前對象調用 notify,全部 listener 都會被執行。而這個對象的 subscribe 或者 notify 的調用時機取決於該對象的使用者。
下面是 subscription 對象的源碼:
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
// 綁定好 this,由於以後會在其餘地方執行
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
addNestedSub(listener) {
// 先執行 trySubscribe 函數,肯定當前實例的訂閱目標(parentSub or store)
this.trySubscribe()
// 子訂閱都集中在 this.listeners 進行管理
return this.listeners.subscribe(listener)
}
notifyNestedSubs() {
this.listeners.notify()
}
handleChangeWrapper() {
// this.onStateChange 由外部提供
if (this.onStateChange) {
this.onStateChange()
}
}
isSubscribed() {
return Boolean(this.unsubscribe)
}
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
複製代碼
咱們先看其構造函數,總共接受了兩個參數:store
和 parentSub
,這個 store 就是你所想到的 store,是 Redux 的數據中心。它在該類的另外一個函數 trySubscribe
中被使用到:
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
複製代碼
能夠看到若是沒有 unsubscribe 屬性的話,會根據是否有 parentSub 屬性進行下一步計算,這說明咱們的 parentSub 是一個可選參數。若是沒有 parentSub 的話就會直接使用 store.subscribe 來訂閱 store 的更新,一旦數據更新,則會執行改類的 handleChangeWrapper 函數。若是有 parentSub 屬性的話,就會執行 parentSub 的 addNestedSub 函數,由於這個函數存在於 Subscription 類上,因此能夠猜測 parentSub 即爲 Subscription 的一個實例。
在執行完 unsubscribe 的初始化以後,會初始化 listeners 的初始化,這裏就用到了咱們以前提到的那個工具函數。
咱們看到訂閱 store 更新的函數是 Subscription.prototype.handleChangeWrapper
:
handleChangeWrapper() {
// this.onStateChange 由外部提供
if (this.onStateChange) {
this.onStateChange()
}
}
複製代碼
而 onStateChange 函數在 Subscription 上並未被定義,只能說明這個函數在使用時被定義。等下咱們閱讀使用這個類的代碼時能夠看到。
咱們再看看 Subscription 是如何使用咱們的 listener 的:
addNestedSub(listener) {
// 先執行 trySubscribe 函數,肯定當前實例的訂閱目標(parentSub or store)
this.trySubscribe()
// 子訂閱都集中在 this.listeners 進行管理
return this.listeners.subscribe(listener)
}
notifyNestedSubs() {
this.listeners.notify()
}
複製代碼
addNestedSub 是 Subscription 的實例做爲另外一個 Subscription 實例的 parentSub 屬性時被調用執行的函數,這個函數會把子 Subscription 實例的 handleChangeWrapper 函數註冊到父 Subscription 實例的 listeners 中,當父 Subscription 實例調用 notifyNestedSub 時,全部的子 Subscription 的 handleChangeWrapper 函數都會被執行。
這就達到了一個目的,React Redux 經過 Subscription 和 listeners 能夠構造一個 Subscription 實例構成的樹,頂部的 Subscription 實例能夠訂閱 store 的變化,store 變化以後會執行 handleChangeWrapper 函數,而若是咱們的 handleChangeWrapper 函數(內部執行 onStateChange)會調用 notifyNestedSub
函數的話,那不就全部的下層 Subscription 實例都會獲得更新的消息?從而子 Subscription 實例的 handleChangeWrapper 函數就會被執行。這是一個由上而下的事件傳遞機制,確保了頂部的事件會被按層級先上後下的傳遞到下層。
示意圖:
經過這張示意圖咱們就能夠很清楚的看到 Subscription 是如何實現事件由上而下派發的機制了。
回過頭繼續看咱們如何實現組件訂閱 Store 數據更新。
const [subscription, notifyNestedSubs] = useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// This Subscription's source should match where store came from: props vs. context. A component
// connected to the store via props shouldn't use subscription from context, or vice versa.
// 若是 store 來自於最底層的 Provider,那麼 parentSub 也要來自於 Provider
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
// the middle of the notification loop, where `subscription` will then be null. This can
// probably be avoided if Subscription's listeners logic is changed to not call listeners
// that have been unsubscribed in the middle of the notification loop.
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
複製代碼
這是在計算當前組件用到的 subscription 實例和 notifyNestedSub 函數。
第一行代碼標識若是用戶配置了不處理 store 變化的話,就不須要 subscription 這個功能了。
後面開始初始化當前組件的 subscription 對象,若是 store 來自於 props,那麼當前組件就是 Subscription 樹最頂層的組件,它沒有 parentSub,它直接訂閱 store 的變化。
若是當前 store 來自於 context,那麼表示它可能不是頂層的 Subscription 實例,須要考慮 contextValue 當中有沒有包含 subscription 屬性,若是有的話就須要將其做爲 parentSub 進行實例化。
最後計算 notifyNestedSub 函數,之因此要綁定是由於像我以前在 Subscription 樹狀圖中畫的那樣,這個函數要做爲 subscription 實例的 handleChangeWrapper 函數調用,因此要確保 this 的指向不變。
這裏容易產生一個疑問,爲何在 contextValue 中會有一個 subscription 實例傳過來呢?咱們在以前查看 Provider 組件源碼的時候也沒看到有這個屬性呀。實際上是後面的代碼重寫了傳遞給下層組件的 contextValue,確保下層被 connect 的組件可以拿到上層組件的 subscription 實例,達到構建 Subscription 樹的目的:
// Determine what {store, subscription} value should be put into nested context, if necessary,
// and memoize that value to avoid unnecessary context updates.
const overriddenContextValue = useMemo(() => {
if (didStoreComeFromProps) {
// This component is directly subscribed to a store from props.
// We don't want descendants reading from this store - pass down whatever
// the existing context value is from the nearest connected ancestor.
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 來源於底層的 Provider,那麼繼續向下一層傳遞 subscription。
return {
...contextValue,
subscription
}
}, [didStoreComeFromProps, contextValue, subscription])
複製代碼
這個 overriddenContextValue 屬性就是被重寫後的 contextValue,能夠看到它把當前組件的 subscription 傳到了下一層,這也就回到了 2.6 小節沒有講完的部分,也就是 ConnectFunction 的最後一段代碼:
// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
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.
return (
<ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider> ) } return renderedWrappedComponent }, [ContextToUse, renderedWrappedComponent, overriddenContextValue]) return renderedChild 複製代碼
若是咱們配置了須要處理 store 更新的話,就會從新使用 Provider 包裹下一層組件(即咱們的 UI 組件),組件接受到的 contextValue 就是咱們重寫後的 contextValue:overriddenContextValue
。
因此下一層組件被 connect 以後,它的 ConnectFunction 就能夠在 contextValue 中拿到它上一層組件的 subscription 對象,這樣就將組件樹關聯起來啦,這是很重要的一步!
組件樹之間的 subscription 樹構建好以後,咱們就須要看看他們之間是如何傳遞事件的。
在 ConnectFunction 內部,定義了一系列 ref:
// Set up refs to coordinate values between the subscription effect and the render logic
const lastChildProps = useRef()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false)
複製代碼
咱們知道 React 的函數組件在每一次渲染的時候都擁有一個獨立的上下文環境(不知道的童鞋能夠閱讀:精讀《useEffect 徹底指南》),爲了防止每次 ConnectFunction 渲染拿不到上一次渲染的相關參數,咱們須要 ref 來進行狀態保留。
這裏的四個 ref 保留了上一次 UI 組件渲染用到的 props、上一次的 wrapperProps 數據以及兩個標誌變量的內容。其中 childPropsFromStoreUpdate 表明因爲 Store 更新致使的新的 props,renderIsScheduled 表明是否須要進行一次 rerender。
// 最終使用的 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.
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
// TODO We're reading the store directly in render() here. Bad idea?
// This will likely cause Bad Things (TM) to happen in Concurrent Mode.
// Note that we do this because on renders _not_ caused by store updates, we need the latest store state
// to determine what the child props should be.
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
複製代碼
上面這是完整的 actualChildProps 的計算過程,不難發現其中關於 childPropsFromStoreUpdate 和 lastWrapperProps 的對比以及其註釋。
這段代碼的做用是若是在計算過程當中因爲 store 的更新致使新的 props 產生,而且當前 wrapperProps 沒有發生變化,那麼就直接使用新的 props,若是 wrapperProps 產生了變化的話就不能直接使用了,由於 wrapperProps 的變化可能致使計算的結果發生變化。
咱們繼續找到 subscription 實例的訂閱代碼:
// Actually subscribe to the nearest connected ancestor (or store)
// 訂閱上層 subscription 的通知,當接受到通知時,說明 store state 發生了變化
// 須要判斷是否 re-render,此時就會執行 checkForUpdates
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
複製代碼
這裏將當前 subscription 的 onStateChange 函數設置成了 checkForUpdates,若是當前 subscription 接收到了 store 更新的消息的話,就會執行 checkForUpdates 函數進行相關狀態的更新以及 rerender。
那咱們繼續找到 checkForUpdates 函數的實現代碼:
// We'll run this callback every time a store subscription update propagates to this component
// 每次收到 store state 更新的通知時,執行這個函數
const checkForUpdates = () => {
if (didUnsubscribe) {
// Don't run stale listeners.
// Redux doesn't guarantee unsubscriptions happen until next dispatch.
return
}
const latestStoreState = store.getState()
let newChildProps, error
try {
// Actually run the selector with the most recent store state and wrapper props
// to determine what the child props should be
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}
if (!error) {
lastThrownError = null
}
// If the child props haven't changed, nothing to do here - cascade the subscription update
if (newChildProps === lastChildProps.current) {
if (!renderIsScheduled.current) {
notifyNestedSubs()
}
} else {
// Save references to the new child props. Note that we track the "child props from store update"
// as a ref instead of a useState/useReducer because we need a way to determine if that value has
// been processed. If this went into useState/useReducer, we couldn't clear out the value without
// forcing another re-render, which we don't want.
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
// 若是須要更新,執行 forceComponentUpdateDispatch 函數強制更新當前組件,這樣就經過 subscription 完成了
// state 的更新和組件的 re-render
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error
}
})
}
}
複製代碼
能夠看到 store 更新事件到達後,會先計算出一個 newChildProps,即咱們新的 props,過程當中若是有計算錯誤會被保存。
若是計算出來的 props 和當前 lastChildProps 引用的結果一致的話,說明數據沒有發生變化,組件若是沒有更新計劃的話就須要手動觸發 notifyNestedSubs 函數通知子組件更新。
若是計算出來的 props 和以前的 props 不相等的話,說明 store 的更新致使 props 發生了變化,須要更新相關引用,並觸發當前組件更新,當前組件更新後 ConnectFunction 的相關計算又開始了新的一輪,因此又回到了咱們以前講的 actualChildProps 數據的計算。這也是爲何在 actualChildProps 的計算過程當中還要考慮 props 和 wrapperProps 的更新。
咱們看到 checkForUpdates 更新當前組件是調用了 forceComponentUpdateDispatch 函數,咱們看看其實現:
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
// We need to force this wrapper component to re-render whenever a Redux store update
// causes a change to the calculated child component props (or we caught an error in mapState)
// 經過 useReducer 強制更新當前組件,由於每次 dispatch 以後 state 都會發生變化
// storeStateUpdatesReducer 返回的數組的第二個參數會一直增長
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
// 若是下層組件在使用時有捕獲到錯誤,則在當前這層拋出
// Propagate any mapState/mapDispatch errors upwards
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
複製代碼
這裏經過 useReducer Hooks 構造了一個數據和一個更新函數。
咱們看到 reducer 是一個每次都會固定更新的函數(updateCount 永遠自增),因此每次調用 forceComponentUpdateDispatch 都會致使當前組件從新渲染。而其數據中 error 的來源就是咱們 checkForUpdates 計算下一次 props 的時候捕捉到的錯誤:
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error
}
})
複製代碼
若是在計算過程當中產生了錯誤,會在下一次渲染的時候拋出來。
咱們須要在組件渲染以後更新以前的引用,因此會有下面這段代碼:
// 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.
// 經過使用 useLayoutEffect 實如今 DOM 更新以後作一些操做,這裏是在 DOM 更新以後更新內部的一些 ref
// 確保下一次判斷時使用的 ref 是最新的
useIsomorphicLayoutEffect(() => {
// We want to capture the wrapper props and child props we used for later comparisons
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false
// If the render was from a store update, clear out that reference and cascade the subscriber update
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
})
複製代碼
同時能夠看到在渲染完成以後,會通知全部子組件 store 數據發生了更新。
值得注意的是 useIsomorphicLayoutEffect
這個自定義 Hook:
// 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 to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
// 參考:https://reactjs.org/docs/hooks-reference.html#uselayouteffect
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect
複製代碼
因爲 useLayoutEffect 不適用於 SSR 的場景,因此會使用 useEffect 做爲 fallback。
講到這裏,React Redux 的工做原理基本就解析完了,文章中爲了不出現大段的代碼已經儘可能少的粘貼源碼,因此可能會致使閱讀起來會存在必定困難。建議你們去個人 GitHub 上面看源碼,包含有相關注釋,有什麼問題也能夠在 issues 提出哦。