React-Redux v5 原始碼分析

Z_Xu發表於2019-01-29

我們知道,Redux是一個獨立的狀態管理工具,如果想要和React搭配使用,就要藉助React-redux。關於它的工作原理網上的教程很多,我大致講一下,之後主要是分析原始碼:

首先,Redux完成的事情:

  1. 負責應用的狀態管理,保證單向資料流,動作可回溯
  2. 每當應用狀態發生變化,觸發所有繫結的監聽器

好了,以上是Redux的工作。那麼,結合React,我們會如何使用呢?

第一個問題,元件如何讀取store中的值

如果我們直接將store作為最外層容器的props傳入,那麼所有子元件都需要去一級一級傳遞這個物件,顯然太過麻煩。ReactRedux這裡使用了在最外層宣告context的方式,供子元件自由讀取。

第二個問題,子元件如何監聽store中state的變化

我們希望當store中某個state發生了變化,只重繪用到了這個state的元件,而不是整個應用。這裡ReactRedux是通過使用高階元件的方式,將你需要連線store的元件包裹起來,通過傳入一系列過濾函式:mapStateToProps, mapDispatchToProps, mergeProps等,以及options中equals判斷函式,最終最小範圍地監聽state變化。監聽到變化後,便會觸發connect中的onStateChange,引起元件的重繪

接下來我們來看原始碼

檔案目錄結構:

React-Redux v5 原始碼分析

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)
  }
}
複製程式碼

參考資料:

  1. https://github.com/reactjs/react-redux/pull/416
  2. http://blog.nicksite.me/index.php/archives/421.html
  3. https://zhuanlan.zhihu.com/p/32407280

相關文章