帶着問題看React-Redux源碼(一萬四千字長文預警)

歡迎關注公衆號: 一口一個前端,不按期分享我所理解的前端知識前端

寫在前面

以前寫了一篇解讀Redux運行機制的文章,以後一直想再寫一篇React-Redux的解析,但這個源碼比較複雜,好在最近收穫了一些東西,分享出來。react

我在讀React-Redux源碼的過程當中,很天然的要去網上找一些參考文章,但發現這些文章基本都沒有講的很透徹,不少時候就是平鋪直敘把API挨個講一下,並且只講某一行代碼是作什麼的,卻沒有結合應用場景和用法解釋清楚爲何這麼作,加上源碼自己又很抽象,函數間的調用關係很是很差梳理清楚,最終結果就是越看越懵。我此次將嘗試換一種解讀方式,由最多見的用法入手,結合用法,提出問題,帶着問題看源碼裏是如何實現的,以此來和你們一塊兒逐漸梳理清楚React-Redux的運行機制。redux

文章用了一週多的時間寫完,粗看了一遍源碼以後,又邊看邊寫。源碼不算少,我儘可能把結構按照最容易理解的方式梳理,努力按照淺顯的方式將原理講出來,但架不住代碼結構的複雜,不少地方依然須要花時間思考,捋清函數之間的調用關係並結合用法才能明白。文章有點長,能看到最後的都是真愛~segmentfault

水平有限,不免有地方解釋的不到位或者有錯誤,也但願你們能幫忙指出來,不勝感激。數組

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提供的組件,這裏作的事情至關於上邊的第一步antd

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將組件包裹起來:app

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變化的時候執行被訂閱到react-redux內的更新函數
  • 提供selector,負責將獲取store中的stat和dispacth一些action的函數(或者直接就是dispatch)或者組件本身的props,從中選擇出組件須要的值,做爲selector的返回值
  • 提供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變化時,通知訂閱者更新頁面,實際上也就是在connect過程當中被訂閱到react-redux的subscrption對象上的更新函數
    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類的時候會講到,
    // 這裏先理解爲最開始的時候須要訂閱更新函數,便於在狀態變化的時候執行更新函數
    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組件就會執行下層組件訂閱到react-redux的更新函數。當Provider更新完成(componentDidUpdate),會去比較一下先後的store是否相同,若是不一樣,那麼用新的store做爲context的值,而且取消訂閱,從新訂閱一個新的Subscription實例。保證用的數據都是最新的。

我猜測應該有一個緣由是考慮到了Provider有可能被嵌套使用,因此會有這種在Provider更新以後取新數據並從新訂閱的作法,這樣才能保證每次傳給子組件的context是最新的。

Provider將執行觸發listeners執行的函數訂閱到了store。

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訂閱,不然,調用context中獲取的subscrption來訂閱,保證都訂閱到一個地方,具體會在下邊說明
      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來實現訂閱的緣由。由於前者的場景是Provider將listener訂閱到store中,此時的listeners數組內實際上是每一個connect內部的checkForUpdates函數(後邊會講到)。後者是connect內部將checkForUpdates放到listeners數組中,其實是利用Provider中傳過來的Subscrption實例來訂閱,保證全部被connect的組件都訂閱到一個Subscrption實例上。

如何向組件中注入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]

      其實是讓whenMapStateToPropsIsFunctionwhenMapStateToPropsIsMissing都去執行一次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的改變作反應。

建立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來實現的。

總結

至此,圍繞經常使用的功能,React-Redux的源碼就解讀完了。回到文章最開始的三個問題:

  • Provider是怎麼把store放入context中的
  • 如何將store中的state和dispatch(或者調用dispatch的函數)注入組件的props中的
  • 咱們都知道在Redux中,能夠經過store.subscribe()訂閱一個更新頁面的函數,來實現store變化,更新UI,而React-Redux是如何作到store變化,被connect的組件也會更新的

如今咱們應該能夠明白,這三個問題對應着React-Redux的三個核心概念:

  • Provider將數據由頂層注入
  • Selector生成組件的props
  • React-Redux的更新機制

它們協同工做也就是React-Redux的運行機制:Provider將數據放入context,connect的時候會從context中取出store,獲取mapStateToProps,mapDispatchToProps,使用selectorFactory生成Selector做爲props注入組件。其次訂閱store的變化,每次更新組件會取到最新的props。

閱讀源碼最好的辦法是先肯定問題,有目的性的去讀。開始的時候我就是硬看,越看越懵,換了一種方式後收穫了很多,相信你也是。

歡迎關注個人公衆號: 一口一個前端,不按期分享我所理解的前端知識

相關文章
相關標籤/搜索