「源碼解析」一文吃透react-redux源碼(useMemo經典源碼級案例)

前言

使用過redux的同窗都知道,redux做爲react公共狀態管理工具,配合react-redux能夠很好的管理數據,派發更新,更新視圖渲染的做用,那麼對於 react-redux 是如何作到根據 state 的改變,而更新組件,促使視圖渲染的呢,讓咱們一塊兒來探討一下,react-redux 源碼的奧妙所在。前端

在正式分析以前咱們不妨來想幾個問題:
vue

1 爲何要在 root 根組件上使用 react-reduxProvider 組件包裹?
2 react-redux 是怎麼和 redux 契合,作到 state 改變動新視圖的呢?
3 provide 用什麼方式存放當前的 reduxstore, 又是怎麼傳遞給每個須要管理state的組件的?
4 connect 是怎麼樣鏈接咱們的業務組件,而後傳遞咱們組件更新函數的呢?
5 connect 是怎麼經過第一個參數,來訂閱與之對應的 state 的呢?
6 connect 怎麼樣將 props,和 reduxstate 合併的?react

帶着這些疑問咱們不妨先看一下 Provider 究竟作了什麼?算法

一 Provider 建立Subscription,context保存上下文

/* provider 組件代碼 */
function Provider({ store, context, children }) {
   /* 利用useMemo,跟據store變化建立出一個contextValue 包含一個根元素訂閱器和當前store */ 
  const contextValue = useMemo(() => {
      /* 建立了一個根 Subscription 訂閱器 */
    const subscription = new Subscription(store)
    /* subscription 的 notifyNestedSubs 方法 ,賦值給 onStateChange方法 */
    subscription.onStateChange = subscription.notifyNestedSubs  
    return {
      store,
      subscription
    } /* store 改變建立新的contextValue */
  }, [store])
  /* 獲取更新以前的state值 ,函數組件裏面的上下文要優先於組件更新渲染 */
  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    /* 觸發trySubscribe方法執行,建立listens */
    subscription.trySubscribe() // 發起訂閱
    if (previousState !== store.getState()) {
        /* 組件更新渲染以後,若是此時state發生改變,那麼當即觸發 subscription.notifyNestedSubs 方法 */
      subscription.notifyNestedSubs() 
    }
    /* */
    return () => {
      subscription.tryUnsubscribe()  // 卸載訂閱
      subscription.onStateChange = null
    }
    /* contextValue state 改變出發新的 effect */
  }, [contextValue, previousState])

  const Context = context || ReactReduxContext
  /* context 存在用跟元素傳進來的context ,若是不存在 createContext建立一個context ,這裏的ReactReduxContext就是由createContext建立出的context */
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

複製代碼

從源碼中provider做用大體是這樣的

1 首先建立一個 contextValue ,裏面包含一個建立出來的父級 Subscription (咱們姑且先稱之爲根級訂閱器)和redux提供的store
2 經過react上下文contextcontextValue 傳遞給子孫組件。redux

二 Subscription訂閱消息,發佈更新

在咱們分析了不是很長的 provider 源碼以後,隨之一個 Subscription 出現,那麼這個 Subscription 由什麼做用呢🤔🤔🤔,咱們先來看看在 Provder 裏出現的Subscription 方法。小程序

notifyNestedSubs trySubscribe tryUnsubscribe設計模式

在整個 react-redux 執行過程當中 Subscription 做用很是重要,這裏方便先透漏一下,他的做用是收集全部被 connect 包裹的組件的更新函數 onstatechange,而後造成一個 callback 鏈表,再由父級 Subscription 統一派發執行更新,咱們暫且不關心它是怎麼運做的,接下來就是 Subscription 源碼 ,咱們重點看一下如上出現的三個方法。前端工程化

/* 發佈訂閱者模式 */
export default class Subscription {
  constructor(store, parentSub) {
    this.store = store
    this.parentSub = parentSub
    this.unsubscribe = null
    this.listeners = nullListeners

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  }
  /* 負責檢測是否該組件訂閱,而後添加訂閱者也就是listener */
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
  /* 向listeners發佈通知 */
  notifyNestedSubs() {
    this.listeners.notify()
  }
  /* 對於 provide onStateChange 就是 notifyNestedSubs 方法,對於 connect 包裹接受更新的組件 ,onStateChange 就是 負責更新組件的函數 。 */
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
   /* 判斷有沒有開啓訂閱 */
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
  /* 開啓訂閱模式 首先判斷當前訂閱器有沒有父級訂閱器 , 若是有父級訂閱器(就是父級Subscription),把本身的handleChangeWrapper放入到監聽者鏈表中 */
  trySubscribe() {
    /* parentSub 便是provide value 裏面的 Subscription 這裏能夠理解爲 父級元素的 Subscription */
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        /* provider的Subscription是不存在parentSub,因此此時trySubscribe 就會調用 store.subscribe */
        : this.store.subscribe(this.handleChangeWrapper)
      this.listeners = createListenerCollection()
    }
  }
  /* 取消訂閱 */
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()

      this.listeners = nullListeners
    }
  }
}


複製代碼

看完 ProviderSubscription源碼,我來解釋一下二者到底有什麼關聯,首先Provider建立 Subscription 時候沒有第二個參數,就說明provider 中的Subscription 不存在 parentSub 。 那麼再調用Provider組件中useEffect鉤子中trySubscribe的時候,會觸發this.store.subscribe , subscribe 就是 reduxsubscribe ,此時真正發起了訂閱。數組

subscription.onStateChange = subscription.notifyNestedSubs 
複製代碼

有此可知,最終state改變,觸發的是notifyNestedSubs方法。咱們再一次看看這個notifyNestedSubs緩存

/* 向listeners發佈通知 */
notifyNestedSubs() {
  this.listeners.notify()
}
複製代碼

最終向當前Subscription 的訂閱者們發佈 notify更新。

Subscription總結 - 發佈訂閱模式的實現

綜上所述咱們總結一下。Subscription 的做用,首先經過 trySubscribe 發起訂閱模式,若是存在這父級訂閱者,就把本身更新函數handleChangeWrapper,傳遞給父級訂閱者,而後父級由 addNestedSub 方法將此時的回調函數(更新函數)添加到當前的 listeners 中 。若是沒有父級元素(Provider的狀況),則將此回調函數放在store.subscribe中,handleChangeWrapper 函數中onStateChange,就是 ProviderSubscriptionnotifyNestedSubs 方法,而 notifyNestedSubs 方法會通知listensnotify 方法來觸發更新。這裏透漏一下,子代Subscription會把更新自身handleChangeWrapper傳遞給parentSub,來統一通知connect組件更新。

這裏咱們弄明白一個問題

react-redux 更新組件也是用了 store.subscribe 並且 store.subscribe 只用在了 ProviderSubscription中 (沒有 parentsub )

大體模型就是

state更改 -> store.subscribe -> 觸發 providerSubscriptionhandleChangeWrapper 也就是 notifyNestedSubs -> 通知 listeners.notify() -> 通知每一個被 connect 容器組件的更新 -> callback 執行 -> 觸發子組件Subscription 的 handleChangeWrapper ->觸發子 onstatechange(能夠提早透漏一下,onstatechange保存了更新組件的函數)。

前邊的內容提到了**createListenerCollection,listeners**,可是他具體有什麼做用咱們接下來一塊兒看一下。

function createListenerCollection() {
   /* batch 由getBatch獲得的 unstable_batchedUpdates 方法 */
  const batch = getBatch()
  let first = null
  let last = null

  return {
    /* 清除當前listeners的全部listener */
    clear() {
      first = null
      last = null
    },
    /* 派發更新 */
    notify() {
      batch(() => {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },
    /* 獲取listeners的全部listener */
    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },
     /* 接收訂閱,將當前的callback(handleChangeWrapper)存到當前的鏈表中 */
    subscribe(callback) {
      let isSubscribed = true

      let listener = (last = {
        callback,
        next: null,
        prev: last
      })

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }
      /* 取消當前 handleChangeWrapper 的訂閱*/
      return function unsubscribe() {
        if (!isSubscribed || first === null) return
        isSubscribed = false

        if (listener.next) {
          listener.next.prev = listener.prev
        } else {
          last = listener.prev
        }
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    }
  }
}
複製代碼

batch

import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
setBatch(batch)
複製代碼

咱們能夠得出結論 createListenerCollection 能夠產生一個 listenerslisteners的做用。

1收集訂閱: 以鏈表的形式收集對應的 listeners (每個Subscription) 的handleChangeWrapper函數。
2派發更新:, 經過 batch 方法( react-dom 中的 unstable_batchedUpdates ) 來進行批量更新。

舒適提示: React unstable_batchedUpdate() API 容許將一次事件循環中的全部 React 更新都一塊兒批量處理到一個渲染過程當中。

總結

🤔到這裏咱們明白了:

1 react-redux 中的 provider 做用 ,經過 reactcontext 傳遞 subscriptionredux 中的store ,而且創建了一個最頂部根 Subscription

2 Subscription 的做用:起到發佈訂閱做用,一方面訂閱 connect 包裹組件的更新函數,一方面經過 store.subscribe 統一派發更新。

3 Subscription 若是存在這父級的狀況,會把自身的更新函數,傳遞給父級 Subscription 來統一訂閱。

三 connect 究竟作了什麼?

1 回顧 connect 用法

工慾善其事,必先利其器 ,想要吃透源碼以前,必須深度熟悉其用法。才能知其然知其因此然。咱們先來看看高階組件connect用法。

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 複製代碼

mapStateToProps

const mapStateToProps = state => ({ todos: state.todos })
複製代碼

做用很簡單,組件依賴reduxstate,映射到業務組件的 props中,state改變觸發,業務組件props改變,觸發業務組件更新視圖。當這個參數沒有的時候,當前組件不會訂閱 store 的改變。

mapDispatchToProps

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' })
  }
}
複製代碼

redux 中的dispatch 方法,映射到,業務組件的props中。

mergeProps

/* * stateProps , state 映射到 props 中的內容 * dispatchProps, dispatch 映射到 props 中的內容。 * ownProps 組件自己的 props */
(stateProps, dispatchProps, ownProps) => Object
複製代碼

正常狀況下,若是沒有這個參數,會按照以下方式進行合併,返回的對象能夠是,咱們自定義的合併規則。咱們還能夠附加一些屬性。

{ ...ownProps, ...stateProps, ...dispatchProps }
複製代碼

options

{
  context?: Object,   // 自定義上下文
  pure?: boolean, // 默認爲 true , 當爲 true 的時候 ,除了 mapStateToProps 和 props ,其餘輸入或者state 改變,均不會更新組件。
  areStatesEqual?: Function, // 當pure true , 比較引進store 中state值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areOwnPropsEqual?: Function, // 當pure true , 比較 props 值, 是否和以前相等。 (next: Object, prev: Object) => boolean
  areStatePropsEqual?: Function, // 當pure true , 比較 mapStateToProps 後的值 是否和以前相等。 (next: Object, prev: Object) => boolean
  areMergedPropsEqual?: Function, // 當 pure 爲 true 時, 比較 通過 mergeProps 合併後的值 , 是否與以前等 (next: Object, prev: Object) => boolean
  forwardRef?: boolean, //當爲true 時候,能夠經過ref 獲取被connect包裹的組件實例。
}
複製代碼

options能夠是如上屬性,上面已經標註了每個屬性的做用,這裏就很少說了。

2 connect 初探

對於connect 組件 ,咱們先看源碼一探究竟

/src/connect/connect.js

export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {}) {
  return function connect( mapStateToProps, mapDispatchToProps, mergeProps, { pure = true, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, ...extraOptions } = {} ) {
   
     /* 通過代理包裝後的 mapStateToProps */
    const initMapStateToProps = match( mapStateToProps, mapStateToPropsFactories,'mapStateToProps' )
    /* 通過代理包裝後的 mapDispatchToProps */
    const initMapDispatchToProps = match(  mapDispatchToProps, mapDispatchToPropsFactories,'mapDispatchToProps')
     /* 通過代理包裝後的 mergeProps */
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

    return connectHOC(selectorFactory, {
     
      methodName: 'connect',
      getDisplayName: name => `Connect(${name})`,
      shouldHandleStateChanges: Boolean(mapStateToProps),
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,
      ...extraOptions
    })
  }
}

export default /*#__PURE__*/ createConnect()
複製代碼

咱們先來分析一下整個函數作的事。

1 首先定一個 createConnect方法。 傳入了幾個默認參數,有兩個參數很是重要,connectHOC 做爲整個 connect 的高階組件。selectorFactory 作爲整合connect更新過程當中的造成新props的主要函數。默認的模式是pure模式。

2 而後執行createConnect方法,返回真正的connect函數自己。connect接收幾個參數,而後和默認的函數進行整合,包裝,代理,最後造成三個真正的初始化函數,這裏的過程咱們就先不講了。咱們接下來分別介紹這三個函數的用途。

initMapStateToProps ,用於造成真正的 MapStateToProps函數,將 store 中 state ,映射到 props

initMapDispatchToProps,用於造成真正的 MapDispatchToProps,將 dispatch 和 自定義的 dispatch 注入到props

initMergeProps,用於造成真正的 mergeProps函數,合併業務組件的 props , state 映射的 props , dispatch 映射的 props

這裏有一個函數很是重要,這個函數就是mergeProps, 請你們記住這個函數,由於這個函數是判斷整個connect是否更新組件關鍵所在。上邊說過 connect基本用法的時候說過,當咱們不向connect傳遞第三個參數mergeProps 的時候,默認的defaultMergeProps以下

/src/connect/mergeProps.js

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}
複製代碼

這個函數返回了一個新的對象,也就是新的props。並且將 業務組件 props , store 中的 state ,和 dispatch 結合到一塊兒,造成一個新對象,做爲新的 props 傳遞給了業務組件。

3 selectorFactory 造成新的props

前面說到selectorFactory 很重要,用於造成新的props,咱們記下來看selectorFactory 源碼。

/src/connect/selectorFactory.js

export default function finalPropsSelectorFactory( dispatch, { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options } ) {
  // mapStateToProps mapDispatchToProps mergeProps 爲真正connect 通過一層代理的 proxy 函數
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  const selectorFactory = options.pure ? pureFinalPropsSelectorFactory : impureFinalPropsSelectorFactory
   // 返回一個 函數用於生成新的 props 
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}
複製代碼

finalPropsSelectorFactory 的代碼很簡單, 首先獲得真正connect 通過一層代理函數 mapStateToProps ,mapDispatchToProps ,mergeProps。而後調用selectorFactory (在pure模式下,selectorFactory 就是 pureFinalPropsSelectorFactory ) 。

能夠這裏反覆用了閉包,能夠剛開始有點蒙,不過靜下心來看發現其實不是很難。因爲默認是pure,因此咱們接下來主要看 pureFinalPropsSelectorFactory 函數作了些什麼。

/** pure組件處理 , 對比 props 是否發生變化 而後 合併props */
export function pureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } //判斷 state prop 是否相等 ) {
  let hasRunAtLeastOnce = false
  let state
  let ownProps
  let stateProps
  let dispatchProps
  let mergedProps
 
  /* 第一次 直接造成 ownProps stateProps dispatchProps 合併 造成新的 props */
  function handleFirstCall(firstState, firstOwnProps) {
    state = firstState
    ownProps = firstOwnProps
    stateProps = mapStateToProps(state, ownProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    hasRunAtLeastOnce = true
    return mergedProps
  }
  
  function handleNewPropsAndNewState() {
    // props 和 state 都改變 mergeProps 
  }

  function handleNewProps() {
    // props 改變 mergeProps
  }

  function handleNewState() {
     // state 改變 mergeProps
  }

  /* 不是第一次的狀況 props 或者 store.state 發生改變的狀況。 */
  function handleSubsequentCalls(nextState, nextOwnProps) {
      /* 判斷兩次 props 是否相等 */
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps) 
      /* 判斷兩次 store.state 是否相等 */
    const stateChanged = !areStatesEqual(nextState, state)
    state = nextState
    ownProps = nextOwnProps
    
    if (propsChanged && stateChanged) return handleNewPropsAndNewState()
    if (propsChanged) return handleNewProps()
    if (stateChanged) return handleNewState()
    return mergedProps
  }

  return function pureFinalPropsSelector(nextState, nextOwnProps) {
    return hasRunAtLeastOnce
      ? handleSubsequentCalls(nextState, nextOwnProps)
      : handleFirstCall(nextState, nextOwnProps)
  }
}
複製代碼

這個函數處理邏輯很清晰。大體上作了這些事。經過閉包的形式返回一個函數pureFinalPropsSelectorpureFinalPropsSelector經過判斷是不是第一次初始化組件。

若是是第一次,那麼直接調用mergeProps合併ownProps,stateProps,dispatchProps 造成最終的props。 若是不是第一次,那麼判斷究竟是props仍是 store.state 發生改變,而後針對那裏變化,從新生成對應的props,最終合併到真正的props

整個 selectorFactory 邏輯就是造成新的props傳遞給咱們的業務組件。

4 connectAdvanced 造成真正包裹業務組件的 Hoc

接下來咱們看一下 connect 返回的 connectAdvanced()到底作了什麼,爲了方便你們理解connect,咱們這裏先看看 connect 用法。

正常模式下:

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

function Index(){
    /* ..... */
    return <div> { /* .... */ } </div>
}
export default connect(mapStateToProp)(Index)
複製代碼

裝飾器模式下:

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

@connect(mapStateToProp)
class Index extends React.Component{
    /* .... */
    render(){
        return <div> { /* .... */ } </div>
    }
}

複製代碼

咱們上面講到,connect執行 接受 mapStateToProp 等參數,最後返回 connectAdvanced() ,那麼上述例子中connect執行第一步connect(mapStateToProp)===connectAdvanced(),也就是connectAdvanced()執行返回真正的hoc,用於包裹咱們的業務組件。

接下來咱們看 connectAdvanced 代碼

/src/components/connectAdvanced.js

export default function connectAdvanced( selectorFactory, // 每次 props,state改變執行 ,用於生成新的 props。 { getDisplayName = name => `ConnectAdvanced(${name})`, //可能被包裝函數(如connect())重寫 methodName = 'connectAdvanced', //若是定義了,則傳遞給包裝元素的屬性的名稱,指示要呈現的調用。用於監視react devtools中沒必要要的從新渲染。 renderCountProp = undefined, shouldHandleStateChanges = true, //肯定此HOC是否訂閱存儲更改 storeKey = 'store', withRef = false, forwardRef = false, // 是否 用 forwarRef 模式 context = ReactReduxContext,// Provider 保存的上下文 ...connectOptions } = {} ) {
  /* ReactReduxContext 就是store存在的context */
  const Context = context
   /* WrappedComponent 爲connect 包裹的組件自己 */   
  return  function wrapWithConnect(WrappedComponent){
      // WrappedComponent 被 connect 的業務組件自己
  }
}
複製代碼

connectAdvanced接受配置參數 , 而後返回真正的 HOC wrapWithConnect

// 咱們能夠講下面的表達式分解
connect(mapStateToProp)(Index)

// 執行 connect
connect(mapStateToProp) 
//返回 
connectAdvanced()
//返回HOC
wrapWithConnect

複製代碼

接下來咱們分析一下wrapWithConnect到底作了些什麼?

5 wrapWithConnect 高階組件

接下來咱們來一塊兒研究一下 wrapWithConnect,咱們重點看一下 wrapWithConnect做爲高階組件,會返回一個組件,這個組件會對原有的業務組件,進行一系列加強等工做。

function wrapWithConnect(WrappedComponent) {
    const wrappedComponentName =
      WrappedComponent.displayName || WrappedComponent.name || 'Component'
  
    const displayName = getDisplayName(wrappedComponentName)
  
    const selectorFactoryOptions = {
      ...connectOptions,
      getDisplayName,
      methodName,
      renderCountProp,
      shouldHandleStateChanges,
      storeKey,
      displayName,
      wrappedComponentName,
      WrappedComponent
    }
    const { pure } = connectOptions
    function createChildSelector(store) {
      // 合併函數 mergeprops 獲得最新的props
      return selectorFactory(store.dispatch, selectorFactoryOptions)
    }
    //判斷是不是pure純組件模式 若是是 將用 useMemo 提高性能
    const usePureOnlyMemo = pure ? useMemo : callback => callback()
    // 負責更新的容器子組件
    function ConnectFunction (props){
        // props 爲 業務組件 真正的 props 
    }
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
  
    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    /* forwardRef */
    if (forwardRef) {
      const forwarded = React.forwardRef(function forwardConnectRef( props, ref ) {
        return <Connect {...props} reactReduxForwardedRef={ref} />
      })
  
      forwarded.displayName = displayName
      forwarded.WrappedComponent = WrappedComponent
      return hoistStatics(forwarded, WrappedComponent)
    }
  
    return hoistStatics(Connect, WrappedComponent)
  }
}
複製代碼

wrapWithConnect 的作的事大體分爲一下幾點:

第一步

1 聲明負責更新的 ConnectFunction 無狀態組件。和負責合併 propscreateChildSelector方法

第二步

2 判斷是不是 pure 純組件模式,若是是用react.memo包裹,這樣作的好處是,會向 pureComponent 同樣對 props 進行淺比較。

第三步

3 若是 connectforwardRef配置項,用React.forwardRef處理,這樣作好處以下。

正常狀況下由於咱們的WrappedComponentconnect 包裝,因此不能經過ref訪問到業務組件WrappedComponent的實例。

子組件

const mapStateToProp = (store) => ({ userInfo: store.root.userInfo })

class Child extends React.Component{
    render(){
        /* ... */
    }
}
export default connect(mapStateToProp)(Child)
複製代碼

父組件

class Father extends React.Compoent{
    child = null 
    render(){
        return <Child ref={(cur)=> this.child = cur } { /* 獲取到的不是`Child`自己 */ } />
    }
}
複製代碼

咱們沒法經過 ref 訪問到 Child 組件。

因此咱們能夠經過 optionsforwardRef 屬性設置爲 true,這樣就能夠根本解決問題。

connect(mapStateToProp,mapDispatchToProps,mergeProps,{ forwardRef:true  })(Child)
複製代碼

第四步

hoistStatics(Connect, WrappedComponent)
複製代碼

最後作的事情就是經過hoistStatics庫 把子組件WrappedComponent的靜態方法/屬性,繼承到父組件Connect上。由於在 高階組件 包裝 業務組件的過程當中,若是不對靜態屬性或是方法加以額外處理,是不會被包裝後的組件訪問到的,因此須要相似hoistStatics這樣的庫,來作處理。

接下來說的就是整個 connect的核心了。咱們來看一下負責更新的容器ConnectFunction 到底作了些什麼?

6 ConnectFunction 控制更新

ConnectFunction 的代碼很複雜,須要咱們一步步去吃透,一步步去消化。

function ConnectFunction(props) {
      /* TODO: 第一步 把 context ForwardedRef props 取出來 */
      const [
        reactReduxForwardedRef,
        wrapperProps // props 傳遞的props
      ] = useMemo(() => {
       
        const { reactReduxForwardedRef, ...wrapperProps } = props
        return [reactReduxForwardedRef, wrapperProps]
      }, [props])
   
  
      // 獲取 context內容 裏面含有 redux 中store 和 subscription
      const contextValue = useContext(Context)

      //TODO: 判斷 store 是否來此 props didStoreComeFromProps ,正常狀況下 ,prop 中是不存在 store 因此 didStoreComeFromProps = false
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)
  
      // 獲取 redux 中 store
      const store = didStoreComeFromProps ? props.store : contextValue.store
       // 返回merge函數 用於生成真正傳給子組件 props
      const childPropsSelector = useMemo(() => {
        return createChildSelector(store)
      }, [store])


      // TODO: 第二步 subscription 監聽者實例 
      const [subscription, notifyNestedSubs] = useMemo(() => {
          // 若是沒有訂閱更新,那麼直接返回。
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
  
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription // 和 上級 `subscription` 創建起關係。 this.parentSub = contextValue.subscription
        )
        // notifyNestedSubs 觸發 noticy 全部子代 listener 監聽者 -> 觸發batch方法,觸發 batchupdate方法 ,批量更新
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )
  
        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])

      /* 建立出一個新的contextValue ,把父級的 subscription 換成本身的 subscription */
      const overriddenContextValue = useMemo(() => {   
        if (didStoreComeFromProps) { 
          return contextValue
        }
        return {
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])
      const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch  /* */
      ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
  

      // TODO: 第三步 
      const lastChildProps = useRef() //保存上一次 合併過的 props信息(通過 ownprops ,stateProps , dispatchProps 合併過的 )
      const lastWrapperProps = useRef(wrapperProps) // 保存本次上下文執行 業務組件的 props 
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false) // 當前組件是否處於渲染階段
      // actualChildProps 爲當前真正處理事後,通過合併的 props
      const actualChildProps = usePureOnlyMemo(() => {
          // 調用 mergeProps 進行合併,返回合併後的最新 porps
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, previousStateUpdateResult, wrapperProps])

     /* 負責更新緩存變量,方便下一次更新的時候比較 */
      useEffect(()=>{
        captureWrapperProps(...[
            lastWrapperProps,
            lastChildProps,
            renderIsScheduled,
            wrapperProps,
            actualChildProps,
            childPropsFromStoreUpdate,
            notifyNestedSubs
         ])
      })
     
      useEffect(()=>{
          subscribeUpdates(...[
          shouldHandleStateChanges,
          store,
          subscription,
          childPropsSelector,
          lastWrapperProps,
          lastChildProps,
          renderIsScheduled,
          childPropsFromStoreUpdate,
          notifyNestedSubs,
          forceComponentUpdateDispatch
         ])
      },[store, subscription, childPropsSelector])



      // TODO: 第四步:reactReduxForwardedRef 是處理父級元素是否含有 forwardRef 的狀況 這裏能夠忽略。
      const renderedWrappedComponent = useMemo(
        () => (
          <WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
      )
      const renderedChild = useMemo(() => {
        //shouldHandleStateChanges 來源 connect是否有第一個參數
        if (shouldHandleStateChanges) {
          return (
            // ContextToUse 傳遞 context 
            <ContextToUse.Provider value={overriddenContextValue}> {renderedWrappedComponent} </ContextToUse.Provider>
          )
        }
  
        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
  
      return renderedChild
    }
複製代碼

爲了方便你們更直觀的理解,我這裏保留了影響流程的核心代碼,我會一步步分析 整個核心部分。想要弄明白這裏,須要對 react-hooksprovider 有一些瞭解。

第一步

經過 props 分離出 reactReduxForwardedRef , wrapperPropsreactReduxForwardedRef 是當開啓 ForwardedRef 模式下,父級傳過來的 React.forwaedRef

而後判斷經過常量didStoreComeFromProps儲存當前,redux.store 是否來自 props, 正常狀況下,咱們的 store 都來自 provider ,不會來自props,因此咱們能夠把didStoreComeFromProps = true 。接下來咱們獲取到 store,經過 store 來判斷是否更新真正的合併props函數childPropsSelector

第二步 建立 子代 subscription, 層層傳遞新的 context(很重要)

這一步很是重要,判斷經過shouldHandleStateChanges判斷此 HOC 是否訂閱存儲更改,若是已經訂閱了更新(此時connect 具備第一個參數),那麼建立一個 subscription ,而且和上一層providersubscription創建起關聯。this.parentSub = contextValue.subscription。而後分離出 subscriptionnotifyNestedSubs(notifyNestedSubs的做用是通知當前subscriptionlisteners 進行更新的方法。 ) 。

而後經過 useMemo 建立出一個新的 contextValue ,把父級的 subscription 換成本身的 subscription。用於經過 Provider 傳遞新的 context這裏簡單介紹一下,運用了 Provider 能夠和多個消費組件有對應關係。多個 Provider 也能夠嵌套使用,裏層的會覆蓋外層的數據。react-reduxcontext更傾向於Provider良好的傳遞上下文的能力。

接下來經過useReducer製造出真正觸發更新的forceComponentUpdateDispatch 函數。也就是整個 state 或者是 props改變,觸發組件更新的函數。 爲何這麼作呢?

筆者認爲react-redxx這樣設計緣由是但願connect本身控制本身的更新,而且多個上下級 connect不收到影響。因此一方面經過useMemo來限制業務組件沒必要要的更新,另外一方面來經過forceComponentUpdateDispatch來更新 HOC 函數,產生actualChildProps,actualChildProps 改變 ,useMemo執行,觸發組件渲染。

第三步:保存信息,執行反作用鉤子(最重要的部分到了)

這一步十分重要,爲何這麼說呢,首先先經過useRef緩存幾個變量:

lastChildProps -> 保存上一次 合併過的 props 信息(通過 ownprops ,stateProps , dispatchProps 合併過的 )。 lastWrapperProps -> 保存本次上下文執行 業務組件的 propsrenderIsScheduled -> 當前組件是否處於渲染階段。 actualChildProps -> actualChildProps 爲當前真正處理事後,通過合併的 props, 組件經過 dep -> actualChildProps,來判斷是否進行更新。

接下來執行兩次 useEffect , 源碼中不是這個樣子的,我這裏通過簡化,第一個 useEffect 執行了 captureWrapperProps ,captureWrapperProps 是幹什麼的呢?

//獲取包裝的props 
function captureWrapperProps( lastWrapperProps, lastChildProps, renderIsScheduled, wrapperProps, actualChildProps, childPropsFromStoreUpdate, notifyNestedSubs ) {
  lastWrapperProps.current = wrapperProps  //子props 
  lastChildProps.current = actualChildProps //通過 megeprops 以後造成的 prop
  renderIsScheduled.current = false  // 當前組件渲染完成
}
複製代碼

captureWrapperProps 的做用很簡單,在一次組件渲染更新後,將上一次 合併前合併後props,保存起來。這麼作目的是,能過在兩次hoc執行渲染中,對比props stateProps是否發生變化。從而肯定是否更新 hoc,進一步更新組件。

執行第二個 useEffect 是很關鍵。執行subscribeUpdates 函數,subscribeUpdates 是訂閱更新的主要函數,咱們一塊兒來看看:

function subscribeUpdates( shouldHandleStateChanges, store, subscription, childPropsSelector, lastWrapperProps, //子props  lastChildProps, //通過 megeprops 以後造成的 prop renderIsScheduled, childPropsFromStoreUpdate, notifyNestedSubs, forceComponentUpdateDispatch ) {
  if (!shouldHandleStateChanges) return

   // 捕獲值以檢查此組件是否卸載以及什麼時候卸載
  let didUnsubscribe = false
  let lastThrownError = null
   //store更新訂閱傳播到此組件時,運行此回調
  const checkForUpdates = ()=>{
      //....
  }
  subscription.onStateChange = checkForUpdates
  //開啓訂閱者 ,當前是被connect 包轉的狀況 會把 當前的 checkForceUpdate 放在存入 父元素的addNestedSub中。
  subscription.trySubscribe()
  //在第一次呈現以後從存儲中提取數據,以防存儲從咱們開始就改變了。
  checkForUpdates()
  /* 卸載訂閱起 */
  const unsubscribeWrapper = () => {
    didUnsubscribe = true
    subscription.tryUnsubscribe()
    subscription.onStateChange = null
  }

  return unsubscribeWrapper
}

複製代碼

這絕對是整個訂閱更新的核心,首先聲明 store 更新訂閱傳播到此組件時的回調函數checkForUpdates把它賦值給onStateChange,若是store中的state發生改變,那麼在組件訂閱了state內容以後,相關聯的state改變就會觸發當前組件的onStateChange,來合併獲得新的props,從而觸發組件更新。

而後subscription.trySubscribe()把訂閱函數onStateChange綁定給父級subscription,進行了層層訂閱。

最後,爲了防止渲染後,store內容已經改變,因此首先執行了一次checkForUpdates。那麼checkForUpdates的做用很明確了,就是檢查是否派發當前組件的更新。

到這裏咱們明白了,react-redux 經過 subscription 進行層層訂閱。對於一層層的組件結構,總體模型圖以下:

接下來咱們看一下checkForUpdates

//store更新訂閱傳播到此組件時,運行此回調
  const checkForUpdates = () => {
    if (didUnsubscribe) {
      //若是寫在了
      return
    }
     // 獲取 store 裏state
    const latestStoreState = store.getState()q
    let newChildProps, error
    try {
      /* 獲得最新的 props */
      newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
      )
    } 
    //若是新的合併的 props沒有更改,則此處不作任何操做-層疊訂閱更新
    if (newChildProps === lastChildProps.current) { 
      if (!renderIsScheduled.current) {  
        notifyNestedSubs() /* 通知子代 subscription 觸發 checkForUpdates 來檢查是否須要更新。 */
      }
    } else {
      lastChildProps.current = newChildProps
      childPropsFromStoreUpdate.current = newChildProps
      renderIsScheduled.current = true
      // 此狀況 可能考慮到 代碼運行到這裏 又發生了 props 更新 因此觸發一個 reducer 來促使組件更新。
      forceComponentUpdateDispatch({
        type: 'STORE_UPDATED',
        payload: {
          error
        }
      })
    }
  }
複製代碼

checkForUpdates 經過調用 childPropsSelector來造成新的props,而後判斷以前的 prop 和當前新的 prop 是否相等。若是相等,證實沒有發生變化,無須更新當前組件,那麼經過調用notifyNestedSubs來通知子代容器組件,檢查是否須要更新。若是不相等證實訂閱的store.state發生變化,那麼當即執行forceComponentUpdateDispatch來觸發組件的更新。

對於層層訂閱的結構,整個更新模型圖以下:

總結

接下來咱們總結一下整個connect的流程。咱們仍是從訂閱更新兩個方向入手。

訂閱流程

整個訂閱的流程是,若是被connect包裹,而且具備第一個參數。首先經過context獲取最近的 subscription,而後建立一個新的subscription,而且和父級的subscription創建起關聯。當第一次hoc容器組件掛在完成後,在useEffect裏,進行訂閱,將本身的訂閱函數checkForUpdates,做爲回調函數,經過trySubscribethis.parentSub.addNestedSub ,加入到父級subscriptionlisteners中。由此完成整個訂閱流程。

更新流程

整個更新流程是,那state改變,會觸發根訂閱器的store.subscribe,而後會觸發listeners.notify ,也就是checkForUpdates函數,而後checkForUpdates函數首先根據mapStoretopropsmergeprops等操做,驗證該組件是否發起訂閱,props 是否改變,並更新,若是發生改變,那麼觸發useReducerforceComponentUpdateDispatch函數,來更新業務組件,若是沒有發生更新,那麼經過調用notifyNestedSubs,來通知當前subscriptionlisteners檢查是否更新,而後盡心層層checkForUpdates,逐級向下,藉此完成整個更新流程。

四 關於 useMemo 用法思考?

整個react-redux源碼中,對於useMemo用法仍是蠻多的,我總結了幾條,奉上🌹🌹:

1 緩存屬性 / 方法

react-redux源碼中,多處應用了useMemo 依賴/緩存 屬性的狀況。這樣作的好處是隻有依賴項發生改變的時候,才更新新的緩存屬性/方法,好比 childPropsSelector , subscription , actualChildProps 等主要方法屬性。

2 控制組件渲染,渲染節流。

react-redux源碼中,經過 useMemo來控制業務組件是否渲染。經過 actualChildProps變化,來證實是否來自 **自身 props ** 或 訂閱的 state 的修改,來肯定是否渲染組件。

例子🌰:

const renderedWrappedComponent = useMemo(
    () => (
        <WrappedComponent {...actualChildProps} ref={reactReduxForwardedRef} />
    ),
    [reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
複製代碼

五 總結

但願這篇文章能讓屏幕前的你,對react-redux的訂閱和更新流程有一個新的認識。送人玫瑰,手留餘香,閱讀的朋友能夠給筆者點贊,關注一波 ,陸續更新前端超硬核文章。

回看筆者往期高贊文章,有更多精彩內容等着你!

vue3.0源碼系列

react-hooks系列

開源項目系列

相關文章
相關標籤/搜索