1. 程式人生 > >React學習筆記_react-redux原理分析

React學習筆記_react-redux原理分析

更多幹貨

寫在前面

之前寫了一篇分析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,mapDispatchthis.props的合併規則,預設合併規則如下:

需要注意的是:如果三個物件中欄位出現同名,前者會被後者覆蓋

如果通過connect註冊了mergeProps方法,以上程式碼會使用mergeProps定義的規則進行合併,mergeProps合併後的結果,會通過props傳入Connect元件。

options

options是一個物件,包含purewithRef兩個屬性

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三者間的關係。