前言
react
和redux
並沒有什麼直接的聯繫.redux
作爲一個通用模塊, 主要還是用來處理應用中的state的變更, 而展示層不一定是react
.但當我們希望在React + Redux的項目中將兩者結合的更好,可以通過
react-redux
做連接。
本文結合react-redux的使用,分析其實現原理。
react-redux
react-redux
是一個輕量級的封裝庫,核心方法只有兩個:
-
Provider
-
connect
下面我們來逐個分析其作用
Provider
Provider模塊的功能並不複雜, 主要分爲以下兩點:
-
在原應用組件上包裹一層,使原來整個應用成爲Provider的子組件
-
接收Redux的store作爲props,通過context對象傳遞給子孫組件上的connect
import { Component, Children } from 'react'
import PropTypes from 'prop-types'
import { storeShape, subscriptionShape } from '../utils/PropTypes'
import warning from '../utils/warning'
let didWarnAboutReceivingStore = false
function warnAboutReceivingStore() {
if (didWarnAboutReceivingStore) {
return
}
didWarnAboutReceivingStore = true
}
export function createProvider(storeKey = 'store', subKey) {
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
this[storeKey] = props.store;
}
render() {
return Children.only(this.props.children)
}
}
if (process.env.NODE_ENV !== 'production') {
Provider.prototype.componentWillReceiveProps = function (nextProps) {
if (this[storeKey] !== nextProps.store) {
warnAboutReceivingStore()
}
}
}
return Provider
}
export default createProvider()
1.1 封裝原應用
render方法中, 渲染了其子級元素, 使整個應用成爲Provider的子組件.
-
this.props.children
是react內置在this.props
上的對象, 用於獲取當前組件的所有子組件. -
Children
爲react內部定義的頂級對象, 該對象封裝了一些方便操作字組件的方法.Children.only
用於獲取僅有的一個子組件,
沒有或者超過一個均會報錯. 所以注意: 確保Provider組件的直接子級爲單個封閉元素,切勿多個組件平行放置
1.2 傳遞store
-
constructor方法: Provider初始化時, 獲取到props中的store對象;
-
getChildContext方法: 將外部的
store
對象放入context
對象中,使子孫組件上的connect
可以直接訪問到context
對象中的store。
context
可以使子孫組件直接獲取父級組件中的數據或方法,而無需一層一層通過props向下傳遞。context
對象相當於一個獨立的空間,父組件通過getChildContext()向該空間內寫值;定義了contextTypes
驗證的子孫組件可以通過this.context.xxx
,從context
對象中讀取xxx
字段的值
1.3 小結
總而言之,Provider
模塊的功能很簡單,從最外部封裝了整個應用,並向connect
模塊傳遞store
。
而最核心的功能在connect
模塊中。
connect
正如這個模塊的命名,
connect
模塊纔是真正連接了React
和Redux
。現在,我們可以先回想一下Redux是怎樣運作的:首先需要註冊一個全局唯一的store對象,用來維護整個應用的state;當要變更state時,我們會dispatch一個action,reducer根據action更新相應的state。
下面我們再考慮一下使用react-redux時,我們做了什麼:
import React from "react"
import ReactDOM from "react-dom"
import { bindActionCreators } from "redux"
import {connect} from "react-redux"
class xxxComponent extends React.Component{
constructor(props){
super(props)
}
componentDidMount(){
this.props.aActions.xxx1();
}
render (
<div>
{this.props.$$aProps}
</div>
)
}
export default connect(
state => ({
$$aProps: state.$$aProps,
$$bProps: state.$$bProps,
// ...
}),
dispatch => ({
aActions: bindActionCreators(AActions,dispatch),
bActions: bindActionCreators(BActions,dispatch),
// ...
})
)(xxxComponent)
由export的component對象進行如下猜想:
1、使用了react-redux
的connect
後,我們導出的對象不再是原先定義的xxx
Component
,而是通過connect
包裹後的新React.Component
對象。connect
執行後返回一個函數(wrapWithConnect),那麼其內部勢必形成了閉包。而wrapWithConnect
執行後,必須要返回一個ReactComponent
對象,才能保證原代碼邏輯可以正常運行,而這個ReactComponent
對象通過render
原組件,形成對原組件的封裝。
2、渲染頁面需要store tree
中的state
片段,變更state
需要dispatch
一個action
,而這兩部分,都是從this.props
獲取。故在我們調用connect
時,作爲參數傳入的state
和action
,便在connect
內部進行合併,通過props的方式傳遞給包裹後的ReactComponent
。
好了, 以上只是我們的猜測, 下面看具體實現, 完整代碼請戳這裏.
connect(
mapStateToProps(state, ownProps) => stateProps: object,
mapDispatchToProps(dispatch, ownProps) => dispatchProps: object,
mergeProps(stateProps, dispatchProps, ownProps) => props: Object,
options: object
) => (
component
) => component
再來看下connect函數體結構, 我們摘取核心步驟進行描述:
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
// 參數處理
// ...
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.store = props.store || context.store;
const storeState = this.store.getState()
this.state = { storeState }
}
// 週期方法及操作方法
// ...
render(){
this.renderedElement = createElement(WrappedComponent,
this.mergedProps //mearge stateProps, dispatchProps, props
)
return this.renderedElement;
}
}
return hoistStatics(Connect, WrappedComponent);
}
}
其實已經基本印證了我們的猜測:
1、connect
通過context
獲取Provider
中的store
,通過store.getState()
獲取整個store
tree
上所有state
。
2、connect
模塊的返回值wrapWithConnect
爲function
。
3、wrapWithConnect
返回一個ReactComponent
對象Connect
,Connect
重新render
外部傳入的原組件WrappedComponent
,並把connect
中傳入的mapStateToProps
, mapDispatchToProps
與組件上原有的props
合併後,通過屬性的方式傳給WrappedComponent
。
下面我們結合代碼進行分析一下每個函數的意義。
mapStateToProps
mapStateToProps(state, props)
必須是一個函數.
參數state
爲store
tree
中所有state, 參數props
爲通過組件Connect
傳入的props
.
返回值表示需要merge
進props
中的state
.
mapDispatchToProps
mapDispatchToProps(dispatch, props)
可以是一個函數, 也可以是一個對象.
參數dispatch
爲store.dispatch
函數,
參數props
爲通過組件Connect
傳入的props
.
返回值表示需要merge
進props
中的action
.
mergeProps(一般不用)
mergeProps
是一個函數,定義了mapState
, mapDispatch
及this.props
的合併規則.
options(一般不用)
options
是一個對象,包含pure
和withRef
兩個屬性pure
: 表示是否開啓pure
優化,默認值爲true.withRef
: withRef
用來給包裝在裏面的組件一個ref
,可以通過getWrappedInstance
方法來獲取這個ref,默認爲false。
React如何響應Store變化
文章一開始我們也提到React其實跟Redux沒有直接聯繫, 也就是說, Redux中dispatch觸發store中state變化, 並不會導致React重新渲染. react-redux纔是真正觸發React重新渲染的模塊, 那麼這一過程怎樣實現的呢?
剛剛提到connect模塊返回一個wrapConnect函數, 此函數中又返回了一個Connect組件. Connect組件的功能有以下兩點:
-
包裝組件, 將state和action通過props的方式傳入到原組件內部
-
監聽store tree變化, 使其包裝的原組件可以響應state變化
下面我們主要分析下第二點
如何註冊監聽
在redux中, 可以通過store.subscribe(listener)註冊一個監聽器.listener會在store tree更新後執行.以下代碼爲Connect組件內部,向store tree註冊listener的過程。
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
this.listeners = createListenerCollection()
}
}
何時註冊
componentDidMount() {
...
this.subscription.trySubscribe()
...
}
可以看到,當Connect組件加載到頁面後,當前組件開始監聽store tree變化
何時註銷
當前Connect組件銷燬後,我們希望其中註冊的listener也一併銷燬,避免性能問題。此時可以在Connect的componentWillUnmount周期函數中執行這一過程。
componentWillUnmount() {
if (this.subscription) this.subscription.tryUnsubscribe()
...
}
變更處理邏輯
有了觸發組件更新的時機,我們下面主要看下,組件是通過何種方式觸發重新渲染
onStateChange() {
...
if (!this.selector.shouldComponentUpdate) {
...
} else {
...
this.setState(dummyState) // dummyState = {}, 僅僅是爲了觸發更新
}
}
小結
可以看到,react-redux的核心功能都在connect模塊中,理解好這個模塊,有助於我們更好的使用react-redux處理業務問題,優化代碼性能。