1. 程式人生 > 實用技巧 >帶著問題看React-Redux原始碼

帶著問題看React-Redux原始碼

React-Redux在專案中的應用

在這裡,我就預設大家已經會使用Redux了,它為我們的應用提供一個全域性的物件(store)來管理狀態。
那麼如何將Redux應用在React中呢?想一下,我們的最終目的是實現跨層級元件間通訊與狀態的統一管理。所以可以使用Context這個特性。

  • 建立一個Provider,將store傳入Provider,作為當前context的值,便於元件通過context獲取Redux store
  • store訂閱一個元件更新的統一邏輯
  • 元件需要更新資料時,需要呼叫store.dispatch派發action,進而觸發訂閱的更新
  • 元件獲取資料時候,使用store.getState()獲取資料

而這些都需要自己手動去做,React-Redux將上邊的都封裝起來了。讓我們通過一段程式碼看一下React-Redux的用法:

首先是在React的最外層應用上,包裹Provider,而Provider是React-Redux提供的元件,這裡做的事情相當於上邊的第一步

import React from 'react'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
const reducer = (state, actions) => {
  ...
}
const store = createStore(reducer)
...

class RootApp extends React.Component {
  render() {
   // 這裡將store傳入Provider
    return <Provider store={store}>
      <App/>
    </Provider>
  }
}

第二步中的訂閱,已經分別在Provider和connect中實現了

再看應用內的子元件。如果需要從store中拿資料或者更新store資料的話(相當於上邊的第三步和第四步),
需要用connect將元件包裹起來:

import React from 'react'
import { connect } from '../../react-redux-src'
import { increaseAction, decreaseAction } from '../../actions/counter'
import { Button } from 'antd'
class Child extends React.Component {
  render() {
    const { increaseAction, decreaseAction, num } = this.props
    return <div
> {num} <Button onClick={() => increaseAction()}>增加</Button> <Button onClick={() => decreaseAction()}>減少</Button> </div> } } const mapStateToProps = (state, ownProps) => { const { counter } = state return { num: counter.num } } const mapDispatchToProps = (dispatch, ownProps) => { return { increaseAction: () => dispatch({ type: INCREASE }), decreaseAction: () => dispatch({ type: DECREASE }) } } export default connect(mapStateToProps, mapDispatchToProps)(Child)

mapStateToProps 用於建立元件和store中儲存的狀態的對映關係,它是一個函式,第一個引數是state,也就是redux中儲存的頂層資料,第二個引數是元件自身的props。返回一個物件,物件內的欄位就是該元件需要從store中獲取的值。

mapDispatchToProps用於建立元件和store.dispatch的對映關係。它可以是一個物件,也可以是一個函式,
當它是一個函式的時候,第一個引數就是dispatch,第二個引數是元件自身的props。

mapDispatchToProps的物件形式如下:

const mapDispatchToProps = {
    increaseAction() {
      return dispatch => dispatch({
        type: INCREASE
      })
    },
    decreaseAction() {
      return dispatch => dispatch({
        type: DECREASE
      })
    }
  }

當不傳mapStateToProps的時候,當store變化的時候,不會引起元件UI的更新。

當不傳mapDispatchToProps的時候,預設將dispatch注入到元件的props中。

以上,如果mapStateToProps 或者mapDispatchToProps傳了ownProps,那麼在元件自身的props變化的時候,這兩個函式也都會被呼叫。

React-Redux做了什麼

我們先給出結論,說明React-Redux做了什麼工作:

  • 提供Subscrption類,實現訂閱更新的邏輯
  • 提供Provider,將store傳入Provider,便於下層元件從context或者props中獲取store;並訂閱store的變化,便於在store變化的時候更新Provider自身
  • 提供selector,負責將獲取store中的stat和dispacth一些action的函式(或者直接就是dispatch)或者元件自己的props,並從中選擇出元件需要的值,作為返回值
  • 提供connect高階元件,主要做了兩件事:

    • 執行selector,獲取到要注入到元件中的值,將它們注入到元件的props
    • 訂閱props的變化,負責在props變化的時候更新元件

如何做的

有了上邊的結論,但想必大家都比較好奇究竟是怎麼實現的,上邊的幾項工作都是協同完成的,最終的表象體現為下面幾個問題:

  • Provider是怎麼把store放入context中的
  • 如何將store中的state和dispatch(或者呼叫dispatch的函式)注入元件的props中的
  • 我們都知道在Redux中,可以通過store.subscribe()訂閱一個更新頁面的函式,來實現store變化,更新UI,而React-Redux是如何做到store變化,被connect的元件也會更新的

接下來,帶著這些問題來一條一條地分析原始碼。

Provider是怎麼把store放入context中的

先從Provider元件入手,程式碼不多,直接上原始碼

class Provider extends Component {
  constructor(props) {
    super(props)
    // 從props中取出store
    const { store } = props
    this.notifySubscribers = this.notifySubscribers.bind(this)
    // 宣告一個Subscription例項。訂閱,監聽state變化來執行listener,都由例項來實現。
    const subscription = new Subscription(store)
    // 繫結監聽,當state變化時,通知訂閱者更新頁面
    subscription.onStateChange = this.notifySubscribers
    // 將store和subscription放入state中,稍後this.state將會作為context的value
    this.state = {
      store,
      subscription
    }
    // 獲取當前的store中的state,作為上一次的state,將會在元件掛載完畢後,
    // 與store新的state比較,不一致的話更新Provider元件
    this.previousState = store.getState()
  }

  componentDidMount() {
    this._isMounted = true

    // 在元件掛載完畢後,訂閱更新。至於如何訂閱的,在下邊講到Subscription類的時候會講到,
    // 這裡先理解為最開始的時候需要訂閱更新函式,便於在狀態變化的時候更新Provider元件
    this.state.subscription.trySubscribe()

    // 如果前後的store中的state有變化,那麼就去更新Provider元件
    if (this.previousState !== this.props.store.getState()) {
      this.state.subscription.notifyNestedSubs()
    }
  }

  componentWillUnmount() {
    // 元件解除安裝的時候,取消訂閱
    if (this.unsubscribe) this.unsubscribe()
    this.state.subscription.tryUnsubscribe()
    this._isMounted = false
  }

  componentDidUpdate(prevProps) {
    // 在元件更新的時候,檢查一下當前的store與之前的store是否一致,若不一致,說明應該根據新的資料做變化,
    // 那麼依照原來的資料做出改變已經沒有意義了,所以會先取消訂閱,再重新宣告Subscription例項,
    // 繫結監聽,設定state為新的資料
    if (this.props.store !== prevProps.store) {
      this.state.subscription.tryUnsubscribe()
      const subscription = new Subscription(this.props.store)
      subscription.onStateChange = this.notifySubscribers
      this.setState({ store: this.props.store, subscription })
    }
  }

  notifySubscribers() {
    // notifyNestedSubs() 實際上會通知讓listener去執行,作用也就是更新UI
    this.state.subscription.notifyNestedSubs()
  }

  render() {
    const Context = this.props.context || ReactReduxContext
    // 將this.state作為context的value傳遞下去
    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

所以結合程式碼看這個問題:Provider是怎麼把store放入context中的,很好理解。
Provider最主要的功能是從props中獲取我們傳入的store,並將store作為context的其中一個值,向下層元件下發。

但是,一旦store變化,Provider要有所反應,以此保證將始終將最新的store放入context中。所以這裡要用訂閱來實現更新。自然引出Subscription類,通過該類的例項,將onStateChange監聽到一個可更新UI的事件
this.notifySubscribers上:

subscription.onStateChange =this.notifySubscribers

元件掛載完成後,去訂閱更新,至於這裡訂閱的是什麼,要看Subscription的實現。這裡先給出結論:本質上訂閱的是onStateChange,實現訂閱的函式是:Subscription類之內的trySubscribe

this.state.subscription.trySubscribe()

再接著,如果前後的state不一樣,那麼就去通知訂閱者更新,onStateChange就會執行,Provider元件就會更新。走到更新完成(componentDidUpdate),
會去比較一下前後的store是否相同,如果不同,那麼用新的store作為context的值,並且取消訂閱,重新訂閱一個新的Subscription例項。保證用的資料都是最新的。

所以說了這麼多,其實這只是Provider元件的更新,而不是應用內部某個被connect的元件的更新機制。我猜想應該有一個原因是考慮到了Provider有可能被巢狀使用,所以會有這種在Provider更新之後取新資料並重新訂閱的做法,這樣才能保證每次傳給子元件的context是最新的。

Subscription

我們已經發現了,Provider元件是通過Subscription類中的方法來實現更新的,而過一會要講到的connect高階元件的更新,也是通過它來實現,可見Subscription是React-Redux實現訂閱更新的核心機制。

import { getBatch } from './batch'
const CLEARED = null
const nullListeners = { notify() {} }

function createListenerCollection() {
  const batch = getBatch()
  let current = []
  let next = []

  return {
    clear() {
      // 清空next和current
      next = CLEARED
      current = CLEARED
    },

    notify() {
      // 將next賦值給current,並同時賦值給listeners,這裡的next、current、listeners其實就是訂閱的更新函式佇列
      const listeners = (current = next)
      // 批量執行listeners
      batch(() => {
        for (let i = 0; i < listeners.length; i++) {
        // 執行更新函式,這是觸發UI更新的最根本的原理
          listeners[i]()
        }
      })
    },

    get() {
      return next
    },

    subscribe(listener) {
      let isSubscribed = true
      // 將current複製一份,並賦值給next,下邊再向next中push  listener(更新頁面的函式)
      if (next === current) next = current.slice()
      next.push(listener)

      return function unsubscribe() {
        if (!isSubscribed || current === CLEARED) return
        isSubscribed = false
        // 最終返回一個取消訂閱的函式,用於在下一輪的時候清除沒用的listener
        if (next === current) next = current.slice()
        next.splice(next.indexOf(listener), 1)
      }
    }
  }
}

export default class Subscription {
  constructor(store, parentSub) {
    // 獲取store,要通過store來實現訂閱
    this.store = store
    // 獲取來自父級的subscription例項,主要是在connect的時候可能會用到
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }

  addNestedSub(listener) {
    this.trySubscribe()
    // 因為這裡是被parentSub呼叫的,所以listener也會被訂閱到parentSub上,也就是從Provider中獲取的subscription
    return this.listeners.subscribe(listener)
  }

  notifyNestedSubs() {
    // 通知listeners去執行
    this.listeners.notify()
  }

  handleChangeWrapper() {
    if (this.onStateChange) {
      // onStateChange會在外部的被例項化成subcription例項的時候,被賦值為不同的更新函式,被賦值的地方分別的Provider和connect中
      // 由於剛剛被訂閱的函式就是handleChangeWrapper,而它也就相當於listener。所以當狀態變化的時候,listener執行,onStateChange會執行
      this.onStateChange()
    }
  }

  isSubscribed() {
    return Boolean(this.unsubscribe)
  }

  trySubscribe() {
    if (!this.unsubscribe) {
      // parentSub實際上是subcription例項
      // 這裡判斷的是this.unsubscribe被賦值後的值,本質上也就是判斷parentSub有沒有,順便再賦值給this.unsubscribe
      // 如果parentSub沒傳,那麼使用store訂閱,否則,呼叫parentSub.addNestedSub,使用React-Redux自己的訂閱邏輯。具體會在程式碼下邊的解釋中說明
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
      // 建立listener集合
      this.listeners = createListenerCollection()
    }
  }

  tryUnsubscribe() {
    // 取消訂閱
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}

Subscription就是將頁面的更新工作和狀態的變化聯絡起來,具體就是listener(觸發頁面更新的方法,在這裡就是handleChangeWrapper),通過trySubscribe方法,根據情況被分別訂閱到store或者Subscription內部。放入到listeners陣列,當state變化的時候,listeners迴圈執行每一個監聽器,觸發頁面更新。

說一下trySubscribe中根據不同情況判斷直接使用store訂閱,還是呼叫addNestedSub來實現內部訂閱的原因。因為可能在一個應用中存在多個store,這裡的判斷是為了讓不同的store訂閱自己的listener,互不干擾。

如何向元件中注入state和dispatch

將store從應用頂層注入後,該考慮如何向元件中注入state和dispatch了。

正常順序肯定是先拿到store,再以某種方式分別執行這兩個函式,將store中的state和dispatch,以及元件自身的props作為mapStateToProps和mapDispatchToProps的引數,傳進去,我們就可以在這兩個函式之內能拿到這些值。而它們的返回值,又會再注入到元件的props中。

說到這裡,就要引出一個概念:selector。最終注入到元件的props是selectorFactory函式生成的selector的返回值,所以也就是說,mapStateToProps和mapDispatchToProps本質上就是selector。

生成的過程是在connect的核心函式connectAdvanced中,這個時候可以拿到當前context中的store,進而用store傳入selectorFactory生成selector,其形式為

function selector(stateOrDispatch, ownProps) {
  ...
  return props
}

通過形式可以看出:selector就相當於mapStateToProps或者mapDispatchToProps,selector的返回值將作為props注入到元件中。

從mapToProps到selector

標題的mapToProps泛指mapStateToProps, mapDispatchToProps, mergeProps

結合日常的使用可知,我們的元件在被connect包裹之後才能拿到state和dispatch,所以我們先帶著上邊的結論,單獨梳理selector的機制,先看connect的原始碼:

export function createConnect({
  connectHOC = connectAdvanced, // connectAdvanced函式是connect的核心
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {...options} = {}
  ) {
    // 將我們傳入的mapStateToProps, mapDispatchToProps, mergeProps都初始化一遍
    const initMapStateToProps = match(mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps')
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
    // 返回connectHOC函式的呼叫,connectHOC的內部是connect的核心
    return connectHOC(selectorFactory, {
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      ...
    })
  }
}

export default createConnect()

connect實際上是createConnect,createConnect也只是返回了一個connect函式,而connect函式返回了connectHOC的呼叫(也就是connectAdvanced的呼叫),再繼續,connectAdvanced的呼叫最終會返回一個wrapWithConnect高階元件,這個函式的引數是我們傳入的元件。所以才有了connect平常的用法:

connect(mapStateToProps, mapDispatchToProps)(Component)

大家應該注意到了connect函式內將mapStateToProps,mapDispatchToProps,mergeProps都初始化了一遍,為什麼要去初始化而不直接使用呢?帶著疑問,我們往下看。

初始化selector過程

先看程式碼,主要看initMapStateToProps 和 initMapDispatchToProps,看一下這段程式碼是什麼意思。

const initMapStateToProps = match(mapStateToProps,
  mapStateToPropsFactories,
  'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps,
  mapDispatchToPropsFactories,
  'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

mapStateToPropsFactories 和 mapDispatchToPropsFactories都是函式陣列,其中的每個函式都會接收一個引數,為mapStateToProps或者mapDispatchToProps。而match函式的作用就是迴圈函式陣列,mapStateToProps或者mapDispatchToProps作為每個函式的入參去執行,當此時的函式返回值不為假的時候,賦值給左側。看一下match函式:

function match(arg, factories, name) {
  // 迴圈執行factories,這裡的factories也就是mapStateToProps和mapDisPatchToProps兩個檔案中暴露出來的處理函式陣列
  for (let i = factories.length - 1; i >= 0; i--) {
    // arg也就是mapStateToProps或者mapDispatchToProps
    // 這裡相當於將陣列內的每個函式之星了一遍,並將我們的mapToProps函式作為引數傳進去
    const result = factories[i](arg)
    if (result) return result
  }
}

match迴圈的是一個函式陣列,下面我們看一下這兩個陣列,分別是mapStateToPropsFactories 和 mapDispatchToPropsFactories:
(下邊原始碼中的whenMapStateToPropsIsFunction函式會放到後邊講解)

  • mapStateToPropsFactories

    • import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
      // 當mapStateToProps是函式的時候,呼叫wrapMapToPropsFunc
      export function whenMapStateToPropsIsFunction(mapStateToProps) {
        return typeof mapStateToProps === 'function'
          ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
          : undefined
      }
      // 當mapStateToProps沒有傳的時候,呼叫wrapMapToPropsConstant
      export function whenMapStateToPropsIsMissing(mapStateToProps) {
        return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
      }
      
      export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]

      實際上是讓whenMapStateToPropsIsFunction和whenMapStateToPropsIsMissing都去執行一次mapStateToProps,然後根據傳入的mapStateToProps的情況來選出有執行結果的函式賦值給initMapStateToProps。

      單獨看一下whenMapStateToPropsIsMissing

      export function wrapMapToPropsConstant(getConstant) {
        return function initConstantSelector(dispatch, options) {
          const constant = getConstant(dispatch, options)
          function constantSelector() {
            return constant
          }
          constantSelector.dependsOnOwnProps = false
          return constantSelector
        }
      }

      wrapMapToPropsConstant返回了一個函式,接收的引數是我們傳入的() => ({}),函式內部呼叫了入參函式並賦值給一個常量放入了constantSelector中,
      該常量實際上就是我們不傳mapStateToProps時候的生成的selector,這個selector返回的是空物件,所以不會接受任何來自store中的state。同時可以看到constantSelector.dependsOnOwnProps = false,表示返回值與connect高階元件接收到的props無關。

  • mapDispatchToPropsFactories

    •     import { bindActionCreators } from '../../redux-src'
          import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
      
          export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
            return typeof mapDispatchToProps === 'function'
              ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
              : undefined
          }
          // 當不傳mapDispatchToProps時,預設向元件中注入dispatch
          export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
            return !mapDispatchToProps
              ? wrapMapToPropsConstant(dispatch => ({ dispatch }))
              : undefined
          }
          // 當傳入的mapDispatchToProps是物件,利用bindActionCreators進行處理  詳見redux/bindActionCreators.js
          export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
            return mapDispatchToProps && typeof mapDispatchToProps === 'object'
              ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch))
              : undefined
          }
      
          export default [
            whenMapDispatchToPropsIsFunction,
            whenMapDispatchToPropsIsMissing,
            whenMapDispatchToPropsIsObject
          ]

      沒有傳遞mapDispatchToProps的時候,會呼叫whenMapDispatchToPropsIsMissing,這個時候,constantSelector只會返回一個dispatch,所以只能在元件中接收到dispatch。

      當傳入的mapDispatchToProps是物件的時候,也是呼叫wrapMapToPropsConstant,根據前邊的瞭解,這裡注入到元件中的屬性是
      bindActionCreators(mapDispatchToProps, dispatch)的執行結果。

現在,讓我們看一下whenMapStateToPropsIsFunction這個函式。它是在mapDispatchToProps與mapStateToProps都是函式的時候呼叫的,實現也比較複雜。這裡只單用mapStateToProps來舉例說明。

再提醒一下:下邊的mapToProps指的是mapDispatchToProps或mapStateToProps

// 根據mapStateToProps函式的引數個數,判斷元件是否應該依賴於自己的props
export function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined
    ? Boolean(mapToProps.dependsOnOwnProps)
    : mapToProps.length !== 1
}

export function wrapMapToPropsFunc(mapToProps, methodName) {
  // 最終wrapMapToPropsFunc返回的是一個proxy函式,返回的函式會在selectorFactory函式中
  // 的finalPropsSelectorFactory內被呼叫並賦值給其他變數。
  // 而這個proxy函式會在selectorFactory中執行,生成最終的selector
  return function initProxySelector(dispatch, { displayName }) {
    const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
      // 根據元件是否依賴自身的props決定呼叫的時候傳什麼引數
      return proxy.dependsOnOwnProps
        ? proxy.mapToProps(stateOrDispatch, ownProps)
        : proxy.mapToProps(stateOrDispatch)
    }
    proxy.dependsOnOwnProps = true
    proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
      // 將proxy.mapToProps賦值為我們傳入的mapToProps
      proxy.mapToProps = mapToProps
      // 根據元件是否傳入了元件本身從父元件接收的props來確定是否需要向元件中注入ownProps,
      // 最終會用來實現元件自身的props變化,也會呼叫mapToProps的效果
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      // 再去執行proxy,這時候proxy.mapToProps已經被賦值為我們傳進來的mapToProps函式,
      // 所以props就會被賦值成傳進來的mapToProps的返回值
      let props = proxy(stateOrDispatch, ownProps)
      if (typeof props === 'function') {
        // 如果返回值是函式,那麼再去執行這個函式,再將store中的state或dispatch,以及ownProps再傳進去
        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
  }
}

wrapMapToPropsFunc返回的實際上是initProxySelector函式,initProxySelector的執行結果是一個代理proxy,可理解為將傳進來的資料(state或dispatch, ownProps)代理到我們傳進來的mapToProps函式。proxy的執行結果是proxy.mapToProps,本質就是selector。

頁面初始化執行的時候,dependsOnOwnProps為true,所以執行proxy.mapToProps(stateOrDispatch, ownProps),也就是detectFactoryAndVerify。在後續的執行過程中,會先將proxy的mapToProps賦值為我們傳入connect的mapStateToProps或者mapDispatchToProps,然後在依照實際情況元件是否應該依賴自己的props賦值給dependsOnOwnProps。(注意,這個變數會在selectorFactory函式中作為元件是否根據自己的props變化執行mapToProps函式的依據)。

總結一下,這個函式最本質上做的事情就是將我們傳入connect的mapToProps函式掛到proxy.mapToProps上,同時再往proxy上掛載一個dependsOnOwnProps來方便區分元件是否依賴自己的props。最後,proxy又被作為initProxySelector的返回值,所以初始化過程被賦值的initMapStateToProps、initMapDispatchToProps、initMergeProps實際上是initProxySelector的函式引用,它們執行之後是proxy,至於它們三個proxy是在哪執行來生成具體的selector的我們下邊會講到。

現在,回想一下我們的疑問,為什麼要去初始化那三個mapToProps函式?目的很明顯,就是準備出生成selector的函式,用來放到一個合適的時機來執行,同時決定selector要不要對ownProps的改變做反應。

資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

建立selector,向元件注入props

準備好了生成selector的函式之後,就需要執行它,將它的返回值作為props注入到元件中了。先粗略的概括一下注入的過程:

  • 取到store的state或dispatch,以及ownProps
  • 執行selector
  • 將執行的返回值注入到元件

下面我們需要從最後一步的注入開始倒推,來看selector是怎麼執行的。

注入的過程發生在connect的核心函式connectAdvanced之內,先忽略該函式內的其他過程,聚焦注入過程,簡單看下原始碼

export default function connectAdvanced(
  selectorFactory,
  {
    getDisplayName = name => `ConnectAdvanced(${name})`,
    methodName = 'connectAdvanced',
    renderCountProp = undefined,
    shouldHandleStateChanges = true,
    storeKey = 'store',
    withRef = false,
    forwardRef = false,
    context = ReactReduxContext,
    ...connectOptions
  } = {}
) {
  const Context = context
  return function wrapWithConnect(WrappedComponent) {

    // ...忽略了其他程式碼

    // selectorFactoryOptions是包含了我們初始化的mapToProps的一系列引數
    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
    // pure表示只有當state或者ownProps變動的時候,重新計算生成selector。
    const { pure } = connectOptions

    /* createChildSelector 的呼叫形式:createChildSelector(store)(state, ownProps),
       createChildSelector返回了selectorFactory的呼叫,而selectorFactory實際上是其內部根據options.pure返回的
       impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory的呼叫,而這兩個函式需要的引數是
           mapStateToProps,
           mapDispatchToProps,
           mergeProps,
           dispatch,
           options
       除了dispatch,其餘引數都可從selectorFactoryOptions中獲得。呼叫的返回值,就是selector。而selector需要的引數是
       (state, ownprops)。所以得出結論,createChildSelector(store)就是selector
    */
    function createChildSelector(store) {
      // 這裡是selectorFactory.js中finalPropsSelectorFactory的呼叫(本質上也就是上面我們初始化的mapToProps的呼叫),傳入dispatch,和options
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }

    function ConnectFunction(props) {
      const store = props.store || contextValue.store
      // 僅當store變化的時候,建立selector
      // 呼叫childPropsSelector => childPropsSelector(dispatch, options)
      const childPropsSelector = useMemo(() => {
        // 每當store變化的時候重新建立這個選擇器
        return createChildSelector(store)
      }, [store])

      // actualChildProps就是最終要注入到元件中的props,也就是selector的返回值。
      const actualChildProps = usePureOnlyMemo(() => {
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

      const renderedWrappedComponent = useMemo(
        // 這裡是將props注入到元件的地方
        () => <WrappedComponent {...actualChildProps} />,
        [forwardedRef, WrappedComponent, actualChildProps]
      )
    }
  // 最後return出去
  return hoistStatics(Connect, WrappedComponent)
}

在注入過程中,有一個很重要的東西:selectorFactory。這個函式就是生成selector的很重要的一環。它起到一個上傳下達的作用,把接收到的dispatch,以及那三個mapToProps函式,傳入到selectorFactory內部的處理函式(pureFinalPropsSelectorFactory 或 impureFinalPropsSelectorFactory)中,selectorFactory的執行結果是內部處理函式的呼叫。而內部處理函式的執行結果就是將那三種selector(mapStateToProps,mapDispatchToProps,mergeProps)

執行後合併的結果。也就是最終要傳給元件的props

下面我們看一下selectorFactory的內部實現。為了清晰,只先一下內部的結構

// 直接將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
export function impureFinalPropsSelectorFactory(){}

export function pureFinalPropsSelectorFactory() {
  // 整個過程首次初始化的時候呼叫
  function handleFirstCall(firstState, firstOwnProps) {}

  // 返回新的props
  function handleNewPropsAndNewState() {
    // 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
  }

  // 返回新的props
  function handleNewProps() {
    // 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
  }

  // 返回新的props
  function handleNewState() {
    // 將mapStateToProps,mapDispatchToProps,ownProps的執行結果合併作為返回值return出去
  }

  // 後續的過程呼叫
  function handleSubsequentCalls(nextState, nextOwnProps) {}

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    // 第一次渲染,呼叫handleFirstCall,之後的action派發行為會觸發handleSubsequentCalls
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}

// finalPropsSelectorFactory函式是在connectAdvaced函式內呼叫的selectorFactory函式
export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  const mapStateToProps = initMapStateToProps(dispatch, options)
  // 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫,是改造
  // 之後的mapStateToProps, 在下邊返回的函式內還會再呼叫一次
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)
  // 根據是否傳入pure屬性,決定呼叫哪個生成selector的函式來計算傳給元件的props。並將匹配到的函式賦值給selectorFactory
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory // 當props或state變化的時候,才去重新計算props
    : impureFinalPropsSelectorFactory // 直接重新計算props

  // 返回selectorFactory的呼叫
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

可以看出來,selectorFactory內部會決定在什麼時候生成新的props。下面來看一下完整的原始碼

export function impureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch
) {
  // 如果呼叫這個函式,直接將三個selector的執行結果合併返回
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}

export function pureFinalPropsSelectorFactory(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  // 使用閉包儲存一個變數,標記是否是第一次執行
  let hasRunAtLeastOnce = false
  // 下邊這些變數用於快取計算結果
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps

  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    // 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫的函式內部的proxy函式的呼叫。
    stateProps = mapStateToProps(state, ownProps)
    /*
    * 膝蓋已爛,太繞了
    * 回顧一下proxy:
    *   const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {}
    *   return proxy
    * */
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    // 返回計算後的props
    return mergedProps
  }

  function handleNewPropsAndNewState() {
    stateProps = mapStateToProps(state, ownProps)
    // 由於這個函式的呼叫條件是ownProps和state都變化,所以有必要判斷一下dependsOnOwnProps
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewProps() {
    // 判斷如果需要依賴元件自己的props,重新計算stateProps
    if (mapStateToProps.dependsOnOwnProps) {
      stateProps = mapStateToProps(state, ownProps)
    }
    // 同上
    if (mapDispatchToProps.dependsOnOwnProps)
      dispatchProps = mapDispatchToProps(dispatch, ownProps)
    // 將元件自己的props,dispatchProps,stateProps整合出來
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleNewState() {
    const nextStateProps = mapStateToProps(state, ownProps)
    const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
    stateProps = nextStateProps
    // 由於handleNewState執行的大前提是pure為true,所以有必要判斷一下前後來自store的state是否變化
    if (statePropsChanged)
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    return mergedProps
  }

  function handleSubsequentCalls(nextState, nextOwnProps) {
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps
    // 依據不同的情況,呼叫不同的函式
    if (propsChanged && stateChanged) return handleNewPropsAndNewState()    // 當元件自己的props和注入的store中的某些state同時變化時,呼叫handleNewPropsAndNewState()獲取最新的props
    if (propsChanged) return handleNewProps() // 僅當元件自己的props變化時,呼叫handleNewProps來獲取最新的props,此時的props包括注入的props,元件自身的props,和dpspatch內的函式
    if (stateChanged) return handleNewState() // 僅當注入的store中的某些state變化時,呼叫handleNewState()獲取最新的props, 此時的props包括注入的props,元件自身的props,和dpspatch內的函式
    // 如果都沒變化,直接返回先前快取的mergedProps,並且在以上三個函式中,都分別用閉包機制對資料做了快取
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    // 第一次渲染,呼叫handleFirstCall,之後的action派發行為會觸發handleSubsequentCalls
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}

export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  const mapStateToProps = initMapStateToProps(dispatch, options) // 這裡是wrapMapToProps.js中wrapMapToPropsFunc函式的柯里化呼叫,是改造
  // 之後的mapStateToProps, 在下邊返回的函式內還會再呼叫一次
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)
  // 驗證mapToProps函式,有錯誤時給出提醒
  if (process.env.NODE_ENV !== 'production') {
    verifySubselectors(
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      options.displayName
    )
  }
  // 根據是否傳入了pure,決定計算新props的方式,預設為true
  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

至此,我們搞明白了mapToProps函式是在什麼時候執行的。再來回顧一下這部分的問題:如何向元件中注入state和dispatch,讓我們從頭梳理一下:

傳入mapToProps

首先,在connect的時候傳入了mapStateToProps,mapDispatchToProps,mergeProps。再聯想一下用法,這些函式內部可以接收到state或dispatch,以及ownProps,它們的返回值會傳入元件的props。

基於mapToProps生成selector

需要根據ownProps決定是否要依據其變化重新計算這些函式的返回值,所以會以這些函式為基礎,生成代理函式(proxy),代理函式的執行結果就是selector,上邊掛載了dependsOnOwnProps屬性,所以在selectorFactory內真正執行的時候,才有何時才去重新計算的依據。

將selector的執行結果作為props傳入元件

這一步在connectAdvanced函式內,建立一個呼叫selectorFactory,將store以及初始化後的mapToProps函式和其他配置傳進去。selectorFactory內執行mapToProps(也就是selector),獲取返回值,最後將這些值傳入元件。

大功告成

React-Redux的更新機制

React-Redux的更新機制也是屬於訂閱釋出的模式。而且與Redux類似,一旦狀態發生變化,呼叫listener更新頁面。讓我們根據這個過程抓取關鍵點:

  • 更新誰?
  • 訂閱的更新函式是什麼?
  • 如何判斷狀態變化?

不著急看程式碼,我覺得先用文字描述清楚這些關鍵問題,不再一頭霧水地看程式碼更容易讓大家理解。

更新誰?

回想一下平時使用React-Redux的時候,是不是隻有被connect過並且傳入了mapStateToProps的元件,會響應store的變化?
所以,被更新的是被connect過的元件,而connect返回的是connectAdvanced,並且並且connectAdvanced會返回我們傳入的元件,
所以本質上是connectAdvanced內部依據store的變化更新自身,進而達到更新真正元件的目的。

訂閱的更新函式是什麼?
這一點從connectAdvanced內部訂閱的時候可以很直觀地看出來:

subscription.onStateChange = checkForUpdates
subscription.trySubscribe()

訂閱的函式是checkForUpdates,重要的是這個checkForUpdates做了什麼,能讓元件更新。在connectAdvanced中使用useReducer內建了一個reducer,這個函式做的事情就是在前置條件(狀態變化)成立的時候,dispatch一個action,來觸發更新。

如何判斷狀態變化?
這個問題很好理解,因為每次redux返回的都是一個新的state。直接判斷前後的state的引用是否相同,就可以了

connect核心--connectAdvanced

connectAdvanced是一個比較重量級的高階函式,上邊大致說了更新機制,但很多具體做法都是在connectAdvanced中實現的。原始碼很長,邏輯有一些複雜,我寫了詳細的註釋。看的過程需要思考函式之間的呼叫關係以及目的,每個變數的意義,帶著上邊的結論,相信不難看懂。

// 這是保留元件的靜態方法的庫
import hoistStatics from 'hoist-non-react-statics'
import React, {
  useContext,
  useMemo,
  useEffect,
  useLayoutEffect,
  useRef,
  useReducer
} from 'react'
import { isValidElementType, isContextConsumer } from 'react-is'
import Subscription from '../utils/Subscription'

import { ReactReduxContext } from './Context'

const EMPTY_ARRAY = []
const NO_SUBSCRIPTION_ARRAY = [null, null]

// 內建的reducer
function storeStateUpdatesReducer(state, action) {
  const [, updateCount] = state
  return [action.payload, updateCount + 1]
}

const initStateUpdates = () => [null, 0]

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
// 自己對於以上英文註釋的意譯:
// 當在服務端環境使用useLayoutEffect時候,react會發出警告,為了解決此問題,需要在服務端使用useEffect,瀏覽器端使用useLayoutEffect。
// useLayoutEffect會在所有的DOM變更之後同步呼叫傳入其中的回撥(effect),
// 所以在瀏覽器環境下需要使用它,因為connect將會在渲染被提交到DOM之後,再同步更新ref來儲存最新的props

// ReactHooks文件對useLayoutEffect的說明:在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。

// useEffect的effect將在每輪渲染結束後執行,useLayoutEffect的effect在dom變更之後,繪製之前執行。
// 這裡的effect做的是更新工作
// 在服務端渲染的時候頁面已經出來了,有可能js還未載入完成。
// 所以需要在SSR階段使用useEffect,保證在頁面由js接管後,如果需要更新了,再去更新。
// 而在瀏覽器環境則不存在這樣的問題

// 根據是否存在window確定是服務端還是瀏覽器端
const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect

export default function connectAdvanced(
  selectorFactory,
  // options object:
  {
    // 獲取被connect包裹之後的元件名
    getDisplayName = name => `ConnectAdvanced(${name})`,

    // 為了報錯資訊的顯示
    methodName = 'connectAdvanced',

    // 直接翻譯的英文註釋:如果被定義, 名為此值的屬性將新增到傳遞給被包裹元件的 props 中。它的值將是元件被渲染的次數,這對於跟蹤不必要的重新渲染非常有用。預設值: undefined
    renderCountProp = undefined,

    // connect元件是否應響應store的變化
    shouldHandleStateChanges = true,

    // 使用了多個store的時候才需要用這個,目的是為了區分該獲取哪個store
    storeKey = 'store',

    // 如果為 true,則將一個引用儲存到被包裹的元件例項中,
    // 並通過 getWrappedInstance()獲取到。
    withRef = false,
    // 用於將ref傳遞進來
    forwardRef = false,

    // 元件內部使用的context,使用者可自定義
    context = ReactReduxContext,

    // 其餘的配置項,selectorFactory應該會用到
    ...connectOptions
  } = {}
) {
  //省略了一些報錯的邏輯

  // 獲取context
  const Context = context

  return function wrapWithConnect(WrappedComponent) {

    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'

    const displayName = getDisplayName(wrappedComponentName)

    // 定義selectorFactoryOptions,為構造selector做準備
    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
    const { pure } = connectOptions
   /* 呼叫createChildSelector => createChildSelector(store)(state, ownProps)
     createChildSelector返回了selectorFactory的帶參呼叫,而selectorFactory實際上是其內部根據options.pure返回的
     impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory的呼叫,而這兩個函式需要的引數是(state, ownProps)
    */
    function createChildSelector(store) {
      // 這裡是selectorFactory.js中finalPropsSelectorFactory的呼叫,傳入dispatch,和options
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }

    // 根據是否是pure模式來決定是否需要對更新的方式做優化,pure在這裡的意義類似於React的PureComponent
    const usePureOnlyMemo = pure ? useMemo : callback => callback()

    function ConnectFunction(props) {
      // props變化,獲取最新的context,forwardedRef以及元件其他props
      const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
        const { context, forwardedRef, ...wrapperProps } = props
        return [context, forwardedRef, wrapperProps]
      }, [props])
      // propsContext或Context發生變化,決定使用哪個context,如果propsContext存在則優先使用
      const ContextToUse = useMemo(() => {
        // 使用者可能會用自定義的context來代替ReactReduxContext,快取住我們應該用哪個context例項
        // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
        // Memoize the check that determines which context instance we should use.
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)
          ? propsContext
          : Context
      }, [propsContext, Context])

      // 通過上層元件獲取上下文中的store
      // 當上層元件最近的context變化的時候,返回該context的當前值,也就是store
      const contextValue = useContext(ContextToUse)
      // store必須存在於prop或者context中
      // 判斷store是否是來自props中的store
      const didStoreComeFromProps = Boolean(props.store)
      // 判斷store是否是來自context中的store
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

      // 從context中取出store,準備被selector處理之後注入到元件。優先使用props中的store
      const store = props.store || contextValue.store
      // 僅當store變化的時候,建立selector
      // childPropsSelector呼叫方式: childPropsSelector(dispatch, options)
      const childPropsSelector = useMemo(() => {
        // selector的建立需要依賴於傳入store
        // 每當store變化的時候重新建立這個selector
        return createChildSelector(store)
      }, [store])

      const [subscription, notifyNestedSubs] = useMemo(() => {
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
        // 如果store是從props中來的,就不再傳入subscription例項,否則使用context中傳入的subscription例項
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription
        )
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )

        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])
      // contextValue就是store,將store重新覆蓋一遍,注入subscription,這樣被connect的元件在context中可以拿到subscription
      const overriddenContextValue = useMemo(() => {
        if (didStoreComeFromProps) {
          // 如果元件是直接訂閱到來自props中的store,就直接使用來自props中的context
          return contextValue
        }

        // Otherwise, put this component's subscription instance into context, so that
        // connected descendants won't update until after this component is done
        // 意譯:
        // 如果store是從context獲取的,那麼將subscription放入上下文,
        // 為了保證在component更新完畢之前被connect的子元件不會更新
        return {
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])

      // 內建reducer,來使元件更新,在checkForUpdates函式中會用到,作為更新機制的核心
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

      if (previousStateUpdateResult && previousStateUpdateResult.error) {
        throw previousStateUpdateResult.error
      }

      // Set up refs to coordinate values between the subscription effect and the render logic
      /*
      * 官方解釋:
      * useRef 返回一個可變的 ref 物件,其 .current 屬性被初始化為傳入的引數(initialValue)。
      * 返回的 ref 物件在元件的整個生命週期內保持不變。
      *
      * ref不僅用於DOM,useRef()的current屬性可以用來儲存值,類似於類的例項屬性
      *
      * */
      const lastChildProps = useRef() // 元件的props,包括來自父級的,store,dispatch
      const lastWrapperProps = useRef(wrapperProps) // 元件本身來自父元件的props
      const childPropsFromStoreUpdate = useRef() // 標記來自store的props是否被更新了
      const renderIsScheduled = useRef(false) // 標記更新的時機
      /*
      * actualChildProps是真正要注入到元件中的props
      * */
      const actualChildProps = usePureOnlyMemo(() => {
        // Tricky logic here:
        // - This render may have been triggered by a Redux store update that produced new child props
        // - However, we may have gotten new wrapper props after that
        // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
        // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
        // So, we'll use the child props from store update only if the wrapper props are the same as last time.
        /*
        * 意譯:
        * 這個渲染將會在store的更新產生新的props時候被觸發,然而,我們可能會在這之後接收到來自父元件的新的props,如果有新的props,
        * 並且來自父元件的props不變,我們應該依據新的child props來更新。但是來自父元件的props更新也會導致整體props的改變,不得不重新計算。
        * 所以只在新的props改變並且來自父元件的props和上次一致(下邊程式碼中的判斷條件成立)的情況下,才去更新
        *
        * 也就是說只依賴於store變動引起的props更新來重新渲染
        * */
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])
      // We need this to execute synchronously every time we re-render. However, React warns
      // about useLayoutEffect in SSR, so we try to detect environment and fall back to
      // just useEffect instead to avoid the warning, since neither will run anyway.
      /*
      * 意譯:我們需要在每次重新渲染的時候同步執行這個effect。但是react將會在SSR的情況放下對於useLayoutEffect做出警告,
      * 所以useIsomorphicLayoutEffect的最終結果是通過環境判斷得出的useEffect或useLayoutEffect。在服務端渲染的時候使用useEffect,
      * 因為在這種情況下useEffect會等到js接管頁面以後再去執行,所以就不會warning了
      * */
      /*
      * 整體看上下有兩個useIsomorphicLayoutEffect,不同之處在於它們兩個的執行時機。
      *
      * 第一個沒有傳入依賴項陣列,所以effect會在每次重新渲染的時候執行,負責每次重新渲染的
      * 時候檢查來自store的資料有沒有變化,變化就通知listeners去更新
      *
      * 第二個依賴於store, subscription, childPropsSelector。所以在這三個變化的時候,去執行effect。
      * 其內部的effect做的事情有別於第一個,負責定義更新函式checkForUpdates、訂閱更新函式,便於在第一個effect響應store更新的時候,
      * 可以將更新函式作為listener執行,來達到更新頁面的目的
      *
      * */

      useIsomorphicLayoutEffect(() => {
        lastWrapperProps.current = wrapperProps // 獲取到元件自己的props
        lastChildProps.current = actualChildProps // 獲取到注入到元件的props
        renderIsScheduled.current = false // 表明已經過了渲染階段
        // If the render was from a store update, clear out that reference and cascade the subscriber update
        // 如果來自store的props更新了,那麼通知listeners去執行,也就是執行先前被訂閱的this.handleChangeWrapper(Subscription類中),
        // handleChangeWrapper中呼叫的是onStateChange,也就是在下邊賦值的負責更新頁面的函式checkForUpdates
        if (childPropsFromStoreUpdate.current) {
          childPropsFromStoreUpdate.current = null
          notifyNestedSubs()
        }
      })

      // Our re-subscribe logic only runs when the store/subscription setup changes
      // 重新訂閱僅在store內的subscription變化時才會執行。這兩個變化了,也就意味著要重新訂閱,因為保證傳遞最新的資料,所以之前的訂閱已經沒有意義了
      useIsomorphicLayoutEffect(() => {
        // 如果沒有訂閱,直接return,shouldHandleStateChanges預設為true,所以預設情況會繼續執行
        if (!shouldHandleStateChanges) return

        // Capture values for checking if and when this component unmounts
        // 當元件解除安裝的時候,用閉包,宣告兩個變數標記是否被取消訂閱和錯誤物件
        let didUnsubscribe = false
        let lastThrownError = null

        // 當store或者subscription變化的時候,回撥會被重新執行,從而實現重新訂閱
        const checkForUpdates = () => {
          if (didUnsubscribe) {
            // 如果取消訂閱了,那啥都不做
            return
          }
          // 獲取到最新的state
          const latestStoreState = store.getState()

          let newChildProps, error
          try {
            // 使用selector獲取到最新的props
            newChildProps = childPropsSelector(
              latestStoreState,
              lastWrapperProps.current
            )
          } catch (e) {
            error = e
            lastThrownError = e
          }

          if (!error) {
            lastThrownError = null
          }

          // 如果props沒變化,只通知一下listeners更新
          if (newChildProps === lastChildProps.current) {
            /*
            * 瀏覽器環境下,useLayoutEffect的執行時機是DOM變更之後,繪製之前。
            * 由於上邊的useIsomorphicLayoutEffect在這個時機執行將renderIsScheduled.current設定為false,
            * 所以會走到判斷內部,保證在正確的時機觸發更新
            *
            * */
            if (!renderIsScheduled.current) {
              notifyNestedSubs()
            }
          } else {
            /*
            * 如果props有變化,將新的props快取起來,並且將childPropsFromStoreUpdate.current設定為新的props,便於在第一個
            * useIsomorphicLayoutEffect執行的時候能夠識別出props確實是更新了
            * */
            lastChildProps.current = newChildProps
            childPropsFromStoreUpdate.current = newChildProps
            renderIsScheduled.current = true
            // 當dispatch 內建的action時候,ConnectFunction這個元件會更新,從而達到更新元件的目的
            forceComponentUpdateDispatch({
              type: 'STORE_UPDATED',
              payload: {
                latestStoreState,
                error
              }
            })
          }
        }

        // onStateChange的角色也就是listener。在provider中,賦值為更新listeners。在ConnectFunction中賦值為checkForUpdates
        // 而checkForUpdates做的工作就是根據props的變化,相當於listener,更新ConnectFunction自身
        subscription.onStateChange = checkForUpdates
        subscription.trySubscribe()

        // 第一次渲染後先執行一次,從store中同步資料
        checkForUpdates()
        // 返回一個取消訂閱的函式,目的是在元件解除安裝時取消訂閱
        const unsubscribeWrapper = () => {
          didUnsubscribe = true
          subscription.tryUnsubscribe()
          if (lastThrownError) {
            throw lastThrownError
          }
        }

        return unsubscribeWrapper
      }, [store, subscription, childPropsSelector])

      // 將元件的props注入到我們傳入的真實元件中
      const renderedWrappedComponent = useMemo(
        () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
        [forwardedRef, WrappedComponent, actualChildProps]
      )

      const renderedChild = useMemo(() => {
        if (shouldHandleStateChanges) {
          // If this component is subscribed to store updates, we need to pass its own
          // subscription instance down to our descendants. That means rendering the same
          // Context instance, and putting a different value into the context.
          /*
          * 意譯:
            如果這個元件訂閱了store的更新,就需要把它自己訂閱的例項往下傳,也就意味這其自身與其
            後代元件都會渲染同一個Context例項,只不過可能會向context中放入不同的值

            再套一層Provider,將被重寫的context放入value。
            這是什麼意思呢?也就是說,有一個被connect的元件,又嵌套了一個被connect的元件,
            保證這兩個從context中獲取的subscription是同一個,而它們可能都會往context中新增加值,
            我加了一個,我的子元件也加了一個。最終的context是所有元件的value的整合,而subscription始終是同一個
          * */
          return (
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>
          )
        }
        // 依賴於接收到的context,傳入的元件,context的value的變化來決定是否重新渲染
        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

      return renderedChild
    }

    // 根據pure決定渲染邏輯
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction

    // 新增元件名
    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    // 如果forwardRef為true,將ref注入到Connect元件,便於獲取到元件的DOM例項
    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef(
        props,
        ref
      ) {
        return <Connect {...props} forwardedRef={ref} />
      })

      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }
    // 保留元件的靜態方法
    return hoistStatics(Connect, WrappedComponent)
  }
}

看完了原始碼,我們整體概括一下React-Redux中被connect的元件的更新機制:
這其中有三個要素必不可少:

  • 根據誰變化(store)
  • 更新函式(checkForUpdates)
  • 將store和更新函式建立聯絡的Subscription

connectAdvanced函式內從context中獲取store,再獲取subscription例項(可能來自context或新建立),然後建立更新函式checkForUpdates,當元件初始化,或者store、Subscription例項、selector變化的時候,訂閱或者重新訂閱。在每次元件更新的時候,檢查一下store是否變化,有變化則通知更新,實際上執行checkForUpdates,本質上呼叫內建reducer更新元件。每次更新導致selector重新計算,所以元件總是能獲取到最新的props。所以說,更新機制的最底層是通過connectAdvanced內建的Reducer來實現的。