[源碼閱讀]高性能和可擴展的React-Redux

React-Redux 源碼

注意:文章很長,只想瞭解邏輯而不深入的,可以直接跳到總結部分

初識

首先,從它暴露對外的API開始

ReactReduxContext
/*
提供了 React.createContext(null)
*/

Provider  
/*
一個儲存數據的組件,渲染了ContextProvider,內部調用redux中store.subscribe
訂閱數據,每當redux中的數據變動,比較新值與舊值,判斷是否重新渲染
*/ 

connect
/* 
一個高階組件,第一階傳入對數據處理方法,第二階傳入要渲染的組件
內部處理了:
1. 對參數的檢查
2. 對傳入的數據處理方法進行處理
(沒傳怎麼處理,傳了提供什麼參數,傳的類型不同怎麼處理,結果如何比較等等)
3. 靜態方法轉移
4. 對渲染組件的傳遞(傳遞給connectAdvanced)
*/

connectAdvanced
/*
保存每一次執行的數據,執行connect定義的方案和邏輯,新舊數據對比(全等對比),渲染組件
這裏作爲公開API,如果我們去使用,那麼connect裏面的邏輯就需要我們自定義了。
*/

現在對它的大概工作範圍有了解後,我們可以開始沿着執行順序分析。


抽絲

Provider

我們使用時,當寫完了redux的reducer, action, bindActionCreators, combineReducers, createStore這一系列內容後,
我們得到了一個store

會先使用<Provider store={store}包裹住根組件。

這時,Provider組件開始工作

componentDidMount() {
  this._isMounted = true
  this.subscribe()
}

第一次加載,需要執行subscribe

subscribe是什麼呢,就是對reduxstore執行subscribe一個自定義函數,
這樣,每當數據變動,這個函數便會執行

subscribe() {
  const { store } = this.props
  // redux 的 store 訂閱
  // 訂閱後,每當state改變 則自動執行這個函數
  this.unsubscribe = store.subscribe(() => {
    // store.getState() 獲取最新的 state
    const newStoreState = store.getState()
    // 組件未加載,取消
    if (!this._isMounted) {
      return
    }
    // 比較state是否相等,全等的不更新
    this.setState(providerState => {
      if (providerState.storeState === newStoreState) {
        return null
      }
      return { storeState: newStoreState }
    })
  })
  /* ... */
}

看到嗎,這個自定義函數非常簡單,每次收到數據,進行全等比較,不等則更新數據。

這個組件的另2個生命週期函數:

componentWillUnmount() {
  if (this.unsubscribe) this.unsubscribe()
  this._isMounted = false
}

componentDidUpdate(prevProps) {
  // 比較store是否相等,如果相等則跳過
  if (this.props.store !== prevProps.store) {
    // 取消訂閱之前的,再訂閱現在的(因爲數據(store)不同了)
    if (this.unsubscribe) this.unsubscribe()
    this.subscribe()
  }
}

這2段的意思就是,每當數據變了,就取消上一次數據的訂閱,在訂閱本次的數據,
當要銷燬組件,取消訂閱。

一段題外話(可跳過):

這個邏輯用HooksuseEffect簡直完美匹配!

useEffect(()=>{
  subscribe()
  return ()=>{
    unSubscribe()
  }
},props.data)

這段的意思就是,當props.data發生改變,執行unSubscribe(),再執行subscribe()

邏輯完全一致有沒有!

最後的render

這裏Context就是React.createContext(null)

<Context.Provider value={this.state}>
  {this.props.children}
</Context.Provider>

到這裏我稱爲react-redux的第一階段。

一個小總結,第一階段就做了1件事:

定義了Provider組件,內部訂閱了store


connect

到主菜了,先看它的export

export default createConnect()

一看,我們應該有個猜測,這貨createConnect是個高階函數。

看看它的參數吧。

export function createConnect({
  connectHOC = connectAdvanced,
  mapStateToPropsFactories = defaultMapStateToPropsFactories,
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  /* ... */
}

題外話:一個編寫默認對象內部含有默認值的方法

function a({x=1,y=2}={}){}

a()      // x:1,y:2
a({})    // x:1,y:2
a({x:2,z:5}) //x:2,y:2

這裏先說明一下它的參數,後面讀起來會很順。

connectHOC: 一個重要組件,用於執行已確定的邏輯,渲染最終組件,後面會詳細說。
mapStateToPropsFactories: 對 mapStateToProps 這個傳入的參數的類型選擇一個合適的方法。
mapDispatchToPropsFactories: 對 mapDispatchToProps 這個傳入的參數的類型選擇一個合適的方法。
mergePropsFactories: 對 mergeProps 這個傳入的參數的類型選擇一個合適的方法。 
selectorFactory: 以上3個只是簡單的返回另一個合適的處理方法,它則執行這些處理方法,並且對結果定義瞭如何比較的邏輯。

可能有點繞,但react-redux就是這麼一個個高階函數組成的,selectorFactory後面會詳細說。

首先我們再次確定這3個名字很長,實際很簡單的函數(源碼這裏不放了)

mapStateToPropsFactories

mapDispatchToPropsFactories

mergePropsFactories

它們只是判斷了參數是否存在,是什麼類型,並且返回一個合適的處理方法,它們並沒有任何處理邏輯。

  • 舉個例子:

    const MyComponent=connect((state)=>state.articles})

    這裏我只定義了mapStateToProps,並且是個function,那麼mapStateToPropsFactories就會返回一個
    處理function的方法。

    我沒有定義mapDispatchToProps,那麼mapDispatchToPropsFactories檢測不到參數,
    則會提供一個默認值dispatch => ({ dispatch }),返回一個處理非function(object)的方法。

那麼處理邏輯是誰定義呢?

wrapMapToProps

wrapMapToProps.js這個文件內部做了以下事情:

  1. 定義了一個處理object的方法(簡單的返回即可,因爲最終目的就是要object)。
  2. 定義了一個處理函數高階函數(執行2次)的方法,這個方法比上面的複雜在於它需要檢測參數是否訂閱了ownProps

檢測方法很簡單,就是檢查參數的length(這裏dependsOnOwnProps是上一次檢查的結果,如果存在則不需要再次檢查)

export function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null &&
    mapToProps.dependsOnOwnProps !== undefined
    ? Boolean(mapToProps.dependsOnOwnProps)
    : mapToProps.length !== 1
}

回到connect,繼續往下看

export function createConnect({
  /* 上面所講的參數 */
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
  /* ... */
  }
}

已經到了我們傳遞參數的地方,前3個參數意思就不解釋了,最後的參數options

areStatesEqual = strictEqual,           // ===比較
areOwnPropsEqual = shallowEqual,        // 淺比較
areStatePropsEqual = shallowEqual,      // 淺比較
areMergedPropsEqual = shallowEqual,     // 淺比較

它們用在selectorFactory這個比較數據結果的方法內部。

繼續往下看

export function createConnect({
  /* 上面已講 */
} = {}) {
  return function connect(
    /* 上面已講 */
  ) {
    const initMapStateToProps = match(
      mapStateToProps,
      mapStateToPropsFactories,
      'mapStateToProps'
    )
    const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

這裏定義了3個變量(函數),match的作用是什麼?

mapStateToProps舉例來說,

因爲上面也說了,mapStateToPropsFactories裏面有多個方法,需要找到一個適合mapStateToProps的,
match就是幹這事了。

match方法內部遍歷mapStateToPropsFactories所有的處理方法,任何一個方法能夠匹配參數mapStateToProps,便被match捕獲返回,
如果一個都找不到則報錯提示參數配置錯誤。

現在這3個變量定義明確了,都是對應的參數的合適的處理方法。

至此,我們已經完成了第二階段,

做個小總結,第二階段做了哪些事:

  1. connect接收了對參數處理方案(3個...Factories)。
  2. connect接收了參數的結果比較方案(selectFactory)
  3. connect接收了參數(mapStateToProps,mapDispatchToProps,mergeProps,options)。
  4. 定義了比較方案(4個are...Equal,其實就是全等比較淺比較)。

前2個階段都是定義階段,接下來需要我們傳入自定義組件,也就是最後一個階段

connect(...)(Component)


接着看connect源碼

export function createConnect({
  /* 上面已講 */
} = {}) {
  return function connect(
    /* 上面已講 */
  ) {
    /* 上面已講 */
    return connectHOC(selectorFactory, {
      // 方法名稱,用在錯誤提示信息
      methodName: 'connect',
      // 最終渲染的組件名稱
      getDisplayName: name => `Connect(${name})`,
      shouldHandleStateChanges: Boolean(mapStateToProps),
      // 以下是傳遞給 selectFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })
  }
}

這裏執行了connectHOC(),傳遞了上面已經講過的參數,而connectHOC = connectAdvanced

因此我們進入最後一個對外APIconnectAdvanced

connectAdvanced

connectAdvanced函數,之前也提過,就是一個執行、組件渲染和組件更新的地方。

它裏面沒有什麼新概念,都是將我們上面講到的參數進行調用,最後根據結果進行渲染新組件。

還是從源碼開始

export default function connectAdvanced(
  selectorFactory,
  {
    // 執行後作用於connect這個HOC組件名稱
    getDisplayName = name => `ConnectAdvanced(${name})`,
    // 用於錯誤提示
    methodName = 'connectAdvanced',
    // 有REMOVED標誌,這裏不關注
    renderCountProp = undefined,
    // 確定connect這個HOC是否訂閱state變動,好像已經沒有用到了
    shouldHandleStateChanges = true,
    // 有REMOVED標誌,這裏不關注
    storeKey = 'store',
    // 有REMOVED標誌,這裏不關注
    withRef = false,
    // 是否通過 forwardRef 暴露出傳入的Component的DOM
    forwardRef = false,
    // React的createContext
    context = ReactReduxContext,

    // 其餘的(比較方法,參數處理方法等)將會傳遞給上面的 selectFactory
    ...connectOptions
  } = {}
) {
  /* ... */
}

參數也沒什麼特別的,有一個forwardRef作用就是能獲取到我們傳入的Component的DOM。
這裏也不深入。

接着看

export default function connectAdvanced(
  /* 上面已講 */
) {
  /* ...對參數的一些驗證和提示哪些參數已經作廢... */
  
  // 定義Context
  const Context = context

  return function wrapWithConnect(WrappedComponent) {
    /* ...檢查 WrappedComponent 是否符合要求... */
   
    /* ...獲取傳入的WrappedComponent的名稱... */
   
    /* ...通過WrappedComponent的名稱計算出當前HOC的名稱... */

    /* ...獲取一些上面的參數(沒有新的參數,都是之前見過的)... */

    // Component就是React.Component
    let OuterBaseComponent = Component
    let FinalWrappedComponent = WrappedComponent

    // 是否純組件
    if (pure) {
      OuterBaseComponent = PureComponent
    }

    /* 定義 makeDerivedPropsSelector 方法,作用後面講 */

    /* 定義 makeChildElementSelector 方法,作用後面講 */

    /* 定義 Connect 組件,作用後面講 */

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    /* ...如果是forWardRef 爲true的情況,此處不深入... */
    
    // 靜態方法轉換
    return hoistStatics(Connect, WrappedComponent)
  }
}

這一段特別長,因此我將不太重要的直接用註釋說明了它們在做什麼,具體代碼就不放了(不重要)。

並且定義了3個新東西,makeDerivedPropsSelectormakeChildElementSelector,Connect

先看最後一句hoistStatics就是hoist-non-react-statics,它的作用是將組件WrappedComponent的所有非React
靜態方法傳遞到Connect內部。

那麼最終它還是返回了一個Connect組件。

Connect組件

這個組件已經是我們寫了完整connect(...)(Component)的返回值了,所以能確定,只要調用<Connect />,就能渲染出一個新的組件出來。

因此它的功能就是確定是否重複更新組件和確定到底更新什麼?

看一個組件,從constructor看起

class Connect extends OuterBaseComponent {
  constructor(props) {
    super(props)
   
    /* ...提示一些無用的參數...*/
    
    this.selectDerivedProps = makeDerivedPropsSelector()
    this.selectChildElement = makeChildElementSelector()
    this.renderWrappedComponent = this.renderWrappedComponent.bind(this)
  }
  /* ... */
}

綁定了一個方法,看名字是render的意思,先不管它。

執行了2個函數。

Connect組件還沒完,這裏先放着,我們先看makeDerivedPropsSelectormakeChildElementSelector

makeDerivedPropsSelector
function makeDerivedPropsSelector() {
  // 閉包儲存上一次的執行結果
  let lastProps
  let lastState
  let lastDerivedProps
  let lastStore
  let sourceSelector

  return function selectDerivedProps(state, props, store) {
    // props和state都和之前相等 直接返回上一次的結果
    if (pure && lastProps === props && lastState === state) {
      return lastDerivedProps
    }

    // 當前store和lastStore不等,更新lastStore
    if (store !== lastStore) {
      lastStore = store
      
      // 終於調用 selectorFactory 了
      sourceSelector = selectorFactory(
        store.dispatch,
        selectorFactoryOptions
      )
    }

    // 更新數據
    lastProps = props
    lastState = state

    // 返回的就是最終的包含所有相應的 state 和 props 的結果
    const nextProps = sourceSelector(state, props)

    // 最終的比較
    if (lastDerivedProps === nextProps) {
      return lastDerivedProps
    }
    lastDerivedProps = nextProps
    return lastDerivedProps
  }
}

大概的說,makeDerivedPropsSelector的執行,先判斷了當前傳入的props(組件的props)state(redux傳入的state)
跟以前的是否全等,如果全等就不需要更新了;

如果不等,則調用了高階函數selectFactory,並且獲得最終數據,最後再判斷最終數據和之前的最終數據是否全等。

爲什麼第一次判斷了,還要判斷第二次,而且都是===判斷?

因爲第一次獲取的stateredux傳入的,是整個APP的所有數據,它們不等說明有組件更新了,但不確定是否是當前組件;

第二次比較的是當前組件的最新數據和以前數據對比。

現在,我們知道selectFactory的作用是獲取當前組件的的最新數據,深入源碼看看。

selectFactory
export default function finalPropsSelectorFactory(
  // redux store的store.dispatch
  dispatch,
  // 3種已經確定了的處理方法
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
  // 返回一個針對用戶傳入的類型的解析函數
  // 例如 mapStateToProps 如果是function,那麼就返回proxy,proxy可以判斷是否需要ownProps,並且對高階函數的 mapStateToProps 進行2次處理,
  // 最終確保返回一個plainObject,否則報錯
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  if (process.env.NODE_ENV !== 'production') {
    verifySubselectors(
      mapStateToProps,
      mapDispatchToProps,
      mergeProps,
      options.displayName
    )
  }

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 默認pure問題true,因此執行 pureFinalPropsSelectorFactory(...)
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )
}

參數就不說了,看註釋。

以下3個,到底返回了什麼,源碼在wrapMapToProps.js上面也說過這個文件內部做了什麼事情。

const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)

這3個調用返回的一個函數,名字叫proxy,這個proxy一旦調用,
就能返回經過mapStateToProps, mapDispatchToProps, mergeProps這3個參數處理過後的數據(plainObject)。

接下來:

const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

  // 默認pure問題true,因此執行 pureFinalPropsSelectorFactory(...)
  return selectorFactory(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    dispatch,
    options
  )

返回了selectorFactory的調用值,也就是pureFinalPropsSelectorFactory(pure默認爲true)。

pureFinalPropsSelectorFactory,它的代碼不少,但邏輯很明瞭,大方向就是對比數據。

這裏關鍵的如何比較不列代碼,只用註釋講明白它的邏輯。

export function pureFinalPropsSelectorFactory(
  // 接受3個proxy方法
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  // 接受3個比較方法
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
  
  /* ...定義變量保存之前的數據(閉包)... */

  function handleFirstCall(firstState, firstOwnProps) {
    /* ...定義第一次執行數據比較的方法,也就是簡單的賦值給上面定義的閉包變量... */
  }

  function handleNewPropsAndNewState() {
    /* 當state和props都有變動時的處理方法 */
  }

  function handleNewProps() {
    /* 當state無變動,props有變動時的處理方法 */
  }

  function handleNewState() {
    /* 當state有變動,props無變動時的處理方法 */
  }

  // 後續數據比較的方法
  function handleSubsequentCalls(nextState, nextOwnProps) {
    // 淺比較
    const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
    // 全等比較
    const stateChanged = !areStatesEqual(nextState, state)
    // 更新數據
    state = nextState
    ownProps = nextOwnProps
    // 當發生不相等的3種情況(關鍵)
    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)
  }
}

上面的閉包變量儲存了上一次的數據,關鍵點就是當和這一次的數據比較後,如果處理更新。

react-redux將它分爲3種情況

  • stateprops都相等。
  • state相等,props不等。
  • state不等,props相等

  • 第一種:stateprops都相等

    • mapStateToProps(proxy):

      不管是否訂閱ownProps,執行mapStateToProps, 因爲state有變動。

    • mapDispatchToProps(proxy):

      只有訂閱了ownProps,纔會執行mapDispatchToProps,因爲state變動與mapDispatchToProps無影響。

    • mergedProps(proxy):

      必定執行,將所有結果合併。

  • 第二種:state相等,props不等

    • mapStateToProps(proxy):

      只有訂閱了ownProps,纔會執行mapStateToProps, 因爲state無變動。

    • mapDispatchToProps(proxy):

      只有訂閱了ownProps,纔會執行mapDispatchToProps,因爲state變動與mapDispatchToProps無影響。

    • mergedProps(proxy):

      必定執行,將所有結果合併。

  • 第三種:state不等,props相等

    • mapStateToProps(proxy):

      不管是否訂閱ownProps,執行mapStateToProps, 因爲state有變動。

      注意,這裏結果需要淺比較判斷

      因爲如果沒有淺比較檢查,而兩者剛好淺比較相等
      那麼最後也會認爲返回一個新的props,也就是相當於重複更新了。

      之所以第一個stateprops都有變動的不需要淺比較檢查,
      是因爲如果props變了,則必須要更新組件。

    • mapDispatchToProps(proxy):

      不會執行,因爲它只關注props

    • mergedProps(proxy):

      只有上面淺比較不等,纔會執行。

makeDerivedPropsSelector的總結:

通過閉包管理數據,並且通過淺比較和全等比較判斷是否需要更新組件數據。

makeChildElementSelector

makeChildElementSelector也是一個高階函數,儲存了之前的數據組件,並且判斷與當前的判斷。

這裏是最終渲染組件的地方,因爲需要判斷一下剛纔最終給出的數據是否需要去更新組件。

2個邏輯:

  1. 數據與之前不等(===),更新組件。
  2. forWardRef屬性值與之前不等,更新組件。

否則,返回舊組件(不更新)。

繼續回到Connect組件。

之後就是render

render() {
  // React的createContext
  const ContextToUse = this.props.context || Context

  return (
    <ContextToUse.Consumer>
      {this.renderWrappedComponent}
    </ContextToUse.Consumer>
  )
}

Context.Consumer內部必須是一個函數,這個函數的參數就是Context.Providervalue,也就是reduxstore

最後一個函數:renderWrappedComponent

renderWrappedComponent(value) {
  /* ...驗證參數有效性... */
  
  // 這裏 storeState=store.getState()
  const { storeState, store } = value

  // 傳入自定義組件的props
  let wrapperProps = this.props
  
  let forwardedRef
  if (forwardRef) {
    wrapperProps = this.props.wrapperProps
    forwardedRef = this.props.forwardedRef
  }

  // 上面已經講了,返回最終數據
  let derivedProps = this.selectDerivedProps(
    storeState,
    wrapperProps,
    store
  )

  // 返回最終渲染的自定義組件
  return this.selectChildElement(derivedProps, forwardedRef)
}

總算結束了,可能有點混亂,做個總結吧。


總結

我把react-redux的執行流程分爲3個階段,分別對應我們的代碼編寫(搭配導圖閱讀)


一張導圖:

react-redux導圖 by stonehank


第一階段:

對應的用戶代碼:

<Provider store={store}>
  <App />
</Provider>

執行內容有:

  1. 定義了Provider組件,這個組件內部訂閱了reduxstore,保證當store發生變動,會立刻執行更新。

第二階段:

對應的用戶代碼:

connect(mapStateToProps,mapDispatchToProps,mergeProps,options)

執行內容有:

  1. connect接收了參數(mapStateToProps,mapDispatchToProps,mergeProps,options)。
  2. connect接收了對參數如何處理方案(3個...Factories)。
  3. connect接收了參數的結果比較方案(selectFactory)
  4. 定義了比較方案(4個are...Equal,其實就是全等比較淺比較)。

第三階段:

對應的用戶代碼:

let newComponent=connect(...)(Component)

<newComponent />

執行內容有:

  1. 接受自定義組件(Component)。
  2. 創建一個Connect組件。
  3. Component的非React靜態方法轉移到Connect
  4. 獲取Provider傳入的數據(redux的整個數據),利用閉包保存數據,用於和未來數據做比較。
  5. 當比較(===)有變動,執行上一階段傳入的參數,獲取當前組件真正的數據。
  6. 利用閉包保存當前組件真正的數據,用於和未來作比較。
  7. 通過全等和淺比較,處理state變動和props變動的邏輯,判斷返回新數據還是舊數據。
  8. 利用閉包保存渲染的組件,通過上面返回的最終數據,判斷需要返回新組件還是就組件。

邏輯理順了,還是很好理解的。

其中第三階段就是對外APIconnectAdvanced的執行內容。


此處查看更多前端源碼閱讀內容。

或許哪一天,我們需要設計一個專用的數據管理系統,那麼就利用好connectAdvanced
我們要做的就是編寫一個自定義第二階段的邏輯體系。

感謝閱讀!

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