寫在前面
之前寫了一篇分析Redux中Store實現的文章(詳見:Redux原理(一):Store實現分析),突然意識到,其實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
下面看下具體代碼:
封裝原應用
[31-34] render方法中,渲染了其子級元素,使整個應用成為Provider的子組件。
1、this.props.children
是react內置在this.props
上的對象,用於獲取當前組件的所有子組件
2、Children
為react內部定義的頂級對象,該對象上封裝了一些方便操作子組件的方法。Children.only
用於獲取僅有的一個子組件,沒有或超過一個均會報錯。故需要注意:確保Provider組件的直接子級為單個封閉元素,切勿多個組件平行放置。
傳遞store
[26-29] Provider初始化時,獲取到props中的store對象;
[22-24] 將外部的store對象放入context對象中,使子孫組件上的connect可以直接訪問到context對象中的store。
1、context
可以使子孫組件直接獲取父級組件中的數據或方法,而無需一層一層通過props向下傳遞。context
對象相當於一個獨立的空間,父組件通過getChildContext()
向該空間內寫值;定義了contextTypes
驗證的子孫組件可以通過this.context.xxx
,從context對象中讀取xxx字段的值。
小結
總而言之,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=>{
return {
$$aProps:state.$$aProps,
$$bProps:state.$$bProps,
// ...
}
},
dispatch=>{
return {
aActions:bindActionCreators(AActions,dispatch),
bActions:bindActionCreators(BActions,dispatch),
// ...
}
}
)(xxxComponent)
通過以上代碼,我們可以歸納出以下信息:
1、使用了react-redux后,我們導出的對象不再是原先定義的xxxComponent,而是通過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完整函數聲明如下:
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。
以上代碼用來計算待merge的state,[104-105]通過調用finalMapStateToProps
獲取merge state。其中作為參數的state通過store.getState()
獲取,很明顯是store tree中所有的state
mapDispatchToProps
mapDispatchToProps(dispatch, props)
可以是一個函數,也可以是一個對象。
參數dispatch為store.dispatch()
函數,參數props為通過組件Connect傳入的props。
返回值表示需要merge進props中的action。
以上代碼用來計算待merge的action,代碼邏輯與計算state十分相似。作為參數的dispatch
就是store.dispatch
。
mergeProps
mergeProps
是一個函數,定義了mapState
,mapDispatch
及this.props
的合並規則,默認合並規則如下:
需要注意的是:如果三個對象中字段出現同名,前者會被后者覆蓋
如果通過connect注冊了mergeProps
方法,以上代碼會使用mergeProps
定義的規則進行合並,mergeProps
合並后的結果,會通過props傳入Connect組件。
options
options
是一個對象,包含pure
和withRef
兩個屬性
pure
表示是否開啟pure優化,默認值為true
withRef
withRef用來給包裝在里面的組件一個ref,可以通過getWrappedInstance方法來獲取這個ref,默認為false。
React如何響應store變化
文章一開始我們也提到React其實跟Redux沒有直接聯系,也就是說,Redux中dispatch觸發store tree中state變化,並不會導致React重新渲染。
react-redux才是真正觸發React重新渲染的模塊,那么這一過程是怎樣實現的呢?
剛剛提到,connect模塊返回一個wrapWithConnect函數,wrapWithConnect函數中又返回了一個Connect組件。Connect組件的功能有以下兩點:
1、包裝原組件,將state和action通過props的方式傳入到原組件內部
2、監聽store tree變化,使其包裝的原組件可以響應state變化
下面我們主要分析下第二點:
如何注冊監聽
Redux中,可以通過store.subscribe(listener)
注冊一個監聽器。listener會在store tree更新后執行。
以上代碼為Connect組件內部,向store tree注冊listener的過程。
[199] 調用store.subscribe
注冊一個名為handleChange
的listener,返回值為當前listener的注銷函數。
何時注冊
可以看到,當Connect組件加載到頁面后,當前組件開始監聽store tree變化。
何時注銷
當當前Connect組件銷毀后,我們希望其中注冊的listener也一並銷毀,避免性能問題。此時可以在Connect的componentWillUnmount
周期函數中執行這一過程。
變更處理邏輯
有了觸發組件更新的時機,我們下面主要看下,組件是通過何種方式觸發重新渲染
[244-245] Connect組件在初始化時,就已經在this.state
中緩存了store tree中state的狀態。這兩行分別取出當前state狀態和變更前state狀態進行比較
[262] 比較過程暫時略過,這一行將最終store tree中state通過this.setState()
更新到Connect內部的state中,而this.setState()
方法正好可以觸發Connect及其子組件的重新渲染。
小結
可以看到,react-redux的核心功能都在connect模塊中,理解好這個模塊,有助於我們更好的使用react-redux處理業務問題,優化代碼性能。
總結
本文通過分析react-redux源碼,詳細介紹了Provider和connect模塊,重新梳理了Reat、redux、react-redux三者間的關系。
個人覺得多看看源碼還是很有好處的,一方面可以加深自己對已使用框架的理解;再一方面可以學到一些優秀的編程思路。
技術這條路上,懂的越多,不懂的也就越多,學無止境,戒驕戒躁。