react-redux原理分析

前言

reactredux並沒有什麼直接的聯繫. 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的子組件.

  1. this.props.children是react內置在this.props上的對象, 用於獲取當前組件的所有子組件.

  2. Children爲react內部定義的頂級對象, 該對象封裝了一些方便操作字組件的方法. Children.only用於獲取僅有的一個子組件,
    沒有或者超過一個均會報錯. 所以注意: 確保Provider組件的直接子級爲單個封閉元素,切勿多個組件平行放置

1.2 傳遞store

  1. constructor方法: Provider初始化時, 獲取到props中的store對象;

  2. getChildContext方法: 將外部的store對象放入context對象中,使子孫組件上的connect可以直接訪問到context對象中的store。

context可以使子孫組件直接獲取父級組件中的數據或方法,而無需一層一層通過props向下傳遞。context對象相當於一個獨立的空間,父組件通過getChildContext()向該空間內寫值;定義了contextTypes驗證的子孫組件可以通過this.context.xxx,從context對象中讀取xxx字段的值

1.3 小結

總而言之,Provider模塊的功能很簡單,從最外部封裝了整個應用,並向connect模塊傳遞store
而最核心的功能在connect模塊中。

connect

正如這個模塊的命名,connect模塊纔是真正連接了ReactRedux

現在,我們可以先回想一下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-reduxconnect後,我們導出的對象不再是原先定義的xxx Component,而是通過connect包裹後的新React.Component對象。
connect執行後返回一個函數(wrapWithConnect),那麼其內部勢必形成了閉包。而wrapWithConnect執行後,必須要返回一個ReactComponent對象,才能保證原代碼邏輯可以正常運行,而這個ReactComponent對象通過render原組件,形成對原組件的封裝。
2、渲染頁面需要store tree中的state片段,變更state需要dispatch一個action,而這兩部分,都是從this.props獲取。故在我們調用connect時,作爲參數傳入的stateaction,便在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模塊的返回值wrapWithConnectfunction
3、wrapWithConnect返回一個ReactComponent對象ConnectConnect重新render外部傳入的原組件WrappedComponent,並把connect中傳入的mapStateToPropsmapDispatchToProps與組件上原有的props合併後,通過屬性的方式傳給WrappedComponent
下面我們結合代碼進行分析一下每個函數的意義。

mapStateToProps

mapStateToProps(state, props)必須是一個函數.
參數statestore tree中所有state, 參數props爲通過組件Connect傳入的props.
返回值表示需要mergeprops中的state.

mapDispatchToProps

mapDispatchToProps(dispatch, props)可以是一個函數, 也可以是一個對象.
參數dispatchstore.dispatch函數, 參數props爲通過組件Connect傳入的props.
返回值表示需要mergeprops中的action.

mergeProps(一般不用)

mergeProps是一個函數,定義了mapStatemapDispatchthis.props的合併規則.

options(一般不用)

options是一個對象,包含purewithRef兩個屬性
pure: 表示是否開啓pure優化,默認值爲true.
withRefwithRef用來給包裝在裏面的組件一個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處理業務問題,優化代碼性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章