我們知道,Redux是一個獨立的狀態管理工具,如果想要和React搭配使用,就要藉助React-redux。關於它的工作原理網上的教程很多,我大致講一下,之後主要是分析原始碼:
首先,Redux完成的事情:
- 負責應用的狀態管理,保證單向資料流,動作可回溯
- 每當應用狀態發生變化,觸發所有繫結的監聽器
好了,以上是Redux的工作。那麼,結合React,我們會如何使用呢?
第一個問題,元件如何讀取store中的值
如果我們直接將store作為最外層容器的props傳入,那麼所有子元件都需要去一級一級傳遞這個物件,顯然太過麻煩。ReactRedux這裡使用了在最外層宣告context的方式,供子元件自由讀取。
第二個問題,子元件如何監聽store中state的變化
我們希望當store中某個state發生了變化,只重繪用到了這個state的元件,而不是整個應用。這裡ReactRedux是通過使用高階元件的方式,將你需要連線store的元件包裹起來,通過傳入一系列過濾函式:mapStateToProps, mapDispatchToProps, mergeProps等,以及options中equals判斷函式,最終最小範圍地監聽state變化。監聽到變化後,便會觸發connect中的onStateChange,引起元件的重繪
接下來我們來看原始碼
檔案目錄結構:
connect.js
// 這裡通過函式呼叫的方式,將預設的工廠函式傳入,是考慮到了擴充套件性和便於測試
export function createConnect({
connectHOC = connectAdvanced, // 這個是最核心的,connect高階元件
mapStateToPropsFactories = defaultMapStateToPropsFactories, // mapStateToProps工廠函式
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, // mapDiaptchToProps工廠函式
mergePropsFactories = defaultMergePropsFactories, // mergePropsFactories工廠函式
selectorFactory = defaultSelectorFactory // selector工廠函式,這裡解釋一下selector的主要作用就是使用上面三個函式,篩選出最後的mergedProps
} = {}) {
// 這裡就是我們實際使用到的connect第一次呼叫函式
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
/**
預設淺比較,這也就是為什麼我們在reducer中通常會這麼寫:
(state = { count: 1 }, action) => {
const nState = cloneDeep(state)
switch (action.type) {
case 'ADD':
nState.count += action.value
}
return nState
}
如果這裡沒有返回一個新的Object,mapStateToProps方法的返回結果 === prevState,則元件不會更新
**/
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, {
// 取個名字,為了在報錯的時候能夠快速定位
methodName: 'connect',
getDisplayName: name => `Connect(${name})`,
// 如果mapStateToProps是個falsy的值,則這個元件就不會去監聽store上state的變化
shouldHandleStateChanges: Boolean(mapStateToProps),
// 這些值都會透傳給selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
...extraOptions
})
}
}
複製程式碼
wrapMapToProps.js
/**
這裡就是match操作主要做的事情了,這裡使用了一個proxy,完成了以下對mapToProps方法的初始化工作:
1. 計算出mapToProps這個函式是否依賴ownProps。
為什麼要去計算是否依賴ownProps呢,每次呼叫mapToProps都將其傳入不好嗎?
因為如果mapToProps不依賴元件的ownProps,可以節省計算。舉個栗子:
某次dispacth操作修改了store上的state,但是state的變化沒有影響到我的元件,只是元件所接受到的props發生了變化。
那麼,如果我的mapToProps不依賴ownProps,我就不需要重新計算mapStateToProps和mapDispatchToProps了
2. 遞迴呼叫mapToProps方法,將它最終返回的那個function,作為真正使用的函式。這個操作只執行一次
3. 在非生產環境,會判斷第一次呼叫mapToProps方法得到結果是不是一個純物件,不是的話會報錯
**/
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)
}
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
// 這裡的操作很關鍵,改變了proxy.mapToProps的指向,使得後面的程式碼只會被執行一次
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
}
}
複製程式碼
// 這是最關鍵的檔案,我們重點看一下
connectAdvanced.js
// 經過前面的分析,我們知道Selector的功能是使用使用者自定義的mapToProps方法,篩選出元件監聽state的範圍,這裡就是Selector真正呼叫的地方了
function makeSelectorStateful(sourceSelector, store) {
const selector = {
run: function runComponentSelector(props) {
try {
// 可以看到,Selector在這類被執行,傳入了新的state和ownProps
const nextProps = sourceSelector(store.getState(), props)
// 如果經過Selector篩選後,返回的nextProps和prevProps的結果淺相同,那麼不執行元件更新
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps // 這裡會快取本次的計算結果,作為下一次計算的prevProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}
return selector
}
export default function connectAdvanced(
selectorFactory,
{
getDisplayName = name => `ConnectAdvanced(${name})`,
methodName = 'connectAdvanced',
renderCountProp = undefined,
shouldHandleStateChanges = true,
storeKey = 'store',
withRef = false,
...connectOptions
} = {}
) {
const subscriptionKey = storeKey + 'Subscription'
const version = hotReloadingVersion++
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.version = version
this.state = {}
this.renderCount = 0
this.store = props[storeKey] || context[storeKey]
this.propsMode = Boolean(props[storeKey]) // 判斷store是否來自props
this.setWrappedInstance = this.setWrappedInstance.bind(this)
this.initSelector() // 初始化Selector
this.initSubscription() // 初始化監聽
}
getChildContext() {
// 判斷如果是propsMode,則不加入subscription訂閱鏈。一般也很少有人會將store作為props引數傳遞吧
const subscription = this.propsMode ? null : this.subscription
// 這裡的動作是將當前元件例項上掛載的subscription物件,新增到context中,以供子元件讀取
// 這樣就保證了所有子元件可以訪問到其父元件的subscrition物件
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
componentDidMount() {
if (!shouldHandleStateChanges) return
// 在componentDidMount方法裡做trySubscribe操作,而不是在componetWillMount,是為了照顧SSR的情況
// 因為在SSR時,componetWillUnmount不會被執行,也就是unsubscribe不會被執行,就會造成記憶體洩露
// 有一種情況下,我們可能在componentWillMount中就dispatch了一個action,修改了state,那麼可能就監聽不到了
// 所以下面會再次執行一遍selector.run方法,保證渲染的準確性
this.subscription.trySubscribe()
this.selector.run(this.props)
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
componentWillUnmount() {
if (this.subscription) this.subscription.tryUnsubscribe()
this.subscription = null
this.notifyNestedSubs = noop
this.store = null
this.selector.run = noop
this.selector.shouldComponentUpdate = false
}
initSelector() {
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions) // 將dispatch和那些mapToProps方法傳入
this.selector = makeSelectorStateful(sourceSelector, this.store) // 暴露一個run方法供呼叫,快取每次的計算結果
this.selector.run(this.props) // 第一次呼叫,進行一些初始化操作,此時不會執行各個equals方法
}
/**
這裡需要重點說一下React-Redux V5的事件訂閱模型:
如果我們把所有的元件的onStateChange事件,訂閱到store.subscribe上,子元件可能受到父元件渲染影響,而導致多次渲染。
React-Redux從5.0版本開始,connect被重寫,增加層級(巢狀)觀察者,保證事件通知與元件更新的順序
更為細緻的分析,可以看這裡: http://blog.nicksite.me/index.php/archives/421.html
**/
initSubscription() {
// 記得我們在呼叫connectHOC方法時,傳入的這個引數嗎,就是判斷了下mapStateToProps是不是一個falsy的值
if (!shouldHandleStateChanges) return
// 獲取父元件的subscription物件
const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
}
// 每次state發生時,被觸發的函式
onStateChange() {
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
// 如果本次元件自身不更新,通知子元件更新
this.notifyNestedSubs()
} else {
// 如果本次元件需要更新,則在更新完成後(didUpdate),通知子元件更新
// 如果本次re-render,同時觸發了子元件的re-render,那麼即便通知更新,子元件也不會重繪了
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
this.setState(dummyState)
}
}
notifyNestedSubsOnComponentDidUpdate() {
this.componentDidUpdate = undefined
this.notifyNestedSubs()
}
isSubscribed() {
return Boolean(this.subscription) && this.subscription.isSubscribed()
}
render() {
const selector = this.selector
selector.shouldComponentUpdate = false
if (selector.error) {
throw selector.error
} else {
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
}
// 這個方法是將wrappedComponent上的非React提供的靜態方法,新增到Connect上
return hoistStatics(Connect, WrappedComponent)
}
}
複製程式碼
參考資料:
- https://github.com/reactjs/react-redux/pull/416
- http://blog.nicksite.me/index.php/archives/421.html
- https://zhuanlan.zhihu.com/p/32407280