React Redux 源碼學習

繼 Redux 源碼學習以後,咱們來看一看 React Redux 是如何將 Redux 和 React 組合起來的。 我正在閱讀的則是 7.2.0 版本代碼。javascript

咱們依然圍繞着 src 進行學習。能夠看到源碼中已經在使用 Hook 代碼。若是你暫時還對 Hook 不是很瞭解的話,建議先前往 React 官網學習 Hook 內容。java

從目錄來看 utils 依然是簡單的工具, hooks 下則是一些對外提供的 Hook , connect 下則是咱們每次都要使用的高階組件函數, components 則也是咱們每次都要使用的 Provider 組件。其中, connect/connect.js 實際上是對 components/connectAdvanced.js 的封裝。react

示例代碼中我會移除並省略掉一些邏輯代碼以及一些我認爲不那麼重要的代碼,即只展示我想要說明的內容。數組

有必要提到的是 utils/batch.jsindex.js 中的部分源碼,其中涉及到 React 協調中的批量更新,而且在 utils/Subscription.js 有使用到。緩存

// utils/batch.js
let batch = callback => callback()

export const setBatch = newBatch => (batch = newBatch)
export const getBatch = () => batch
// index.js
import { setBatch } from './utils/batch'
import { unstable_batchedUpdates as batch } from 'react-dom'

setBatch(batch)
複製代碼

Subscription

從上面咱們能夠看出 React Redux 這裏使用的更新模式並非「普通」的執行函數,而是依賴於 React DOM 的批量更新,做爲了解學習便可,能夠增進一些本身的思路。如今,讓咱們來看到 utils/Subscription.js 的源碼:性能優化

function createListenerCollection() {
  // 依賴於 React DOM 的批量更新
  const batch = getBatch()
  // 監聽器鏈表頭和尾
  let first = null
  let last = null
  // 典型的閉包結構
  return {
    // 清空鏈表
    clear() {
      first = null
      last = null
    },
    // 通知函數
    notify() {
      // 調用依賴於 React DOM 的批量更新
      batch(() => {
        let listener = first
        while (listener) {
          listener.callback()
          listener = listener.next
        }
      })
    },
    // 獲取監聽器列表
    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },
    // 訂閱回調函數,添加至監聽器鏈表
    subscribe(callback) {
      let isSubscribed = true

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

      if (listener.prev) {
        listener.prev.next = listener
      } else {
        first = listener
      }
      // 返回取消訂閱函數(典型的閉包結構)
      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
        }
      }
    }
  }
}

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)
  }
  // 添加監聽器回調
  addNestedSub(listener) {
    this.trySubscribe()
    return this.listeners.subscribe(listener)
  }
  // 監聽器通知
  notifyNestedSubs() {
    this.listeners.notify()
  }
  // 處理指定 API onStateChange
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange()
    }
  }
  // 是否嘗試過訂閱
  isSubscribed() {
    return Boolean(this.unsubscribe)
  }
  // 訂閱
  trySubscribe() {
    if (!this.unsubscribe) {
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper)
      // 建立監聽器集合
      this.listeners = createListenerCollection()
    }
  }
  // 退訂清除緩存
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe()
      this.unsubscribe = null
      this.listeners.clear()
      this.listeners = nullListeners
    }
  }
}
複製代碼

Provider

上面是一個訂閱器的簡單實現,也是 Provider 組件觸發更新的原理,那麼話很少說,咱們來看到 components 目錄下 Provider.js 的相關源碼:markdown

const ReactReduxContext = /*#__PURE__*/ React.createContext(null)

function Provider({ store, context, children }) {
  const contextValue = useMemo(() => {
    // 實例化 Subscription
    const subscription = new Subscription(store)
    // API onStateChange 設置
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription
    }
  }, [store])
  // Redux state 獲取 API 調用 
  const previousState = useMemo(() => store.getState(), [store])

  useEffect(() => {
    const { subscription } = contextValue
    // 嘗試訂閱並建立監聽器集合
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      // 若新舊 state 不同則發起通知,執行監聽器列表
      subscription.notifyNestedSubs()
    }
    return () => {
      // 清除反作用
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])
  // 用戶自定義 Context 優先
  const Context = context || ReactReduxContext
  // contextValue 包含整個 Redux 實例和訂閱器
  return <Context.Provider value={contextValue}>{children}</Context.Provider> } 複製代碼

connect

看到這裏其實已經明白 Provider 組件的整個邏輯,即依據 storepreviousState 的變化觸發訂閱退訂的生命週期以及通知更新操做。那麼再和 connect 高階組件函數配合實現 React Redux 的功能。顯然,到了這裏我想你們都應該能猜到, Provider 組件用於注入 Redux 並控制邏輯,connect 高階組件函數來處理用戶想要的 storeprops 的形式傳入。咱們先來看到 connectAdvanced.js 的源碼吧(下面內容有些多,我陪你一點點看下去):閉包

export default function connectAdvanced(
  /*
    選擇器工廠函數(默認官方提供)做用是生成相似這樣的代碼內容:
    export default connectAdvanced((dispatch, options) => (state, props) => ({
      thing: state.things[props.thingId],
      saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
    }))(YourComponent)
  */
  selectorFactory,
  // 配置參數,官方已在 connect 進行封裝(固然用戶能夠本身魔改),因此咱們在使用的時候用的是 connect
  {
    // the func used to compute this HOC's displayName from the wrapped component's displayName.
    // probably overridden by wrapper functions such as connect()
    getDisplayName = name => `ConnectAdvanced(${name})`,
    // shown in error messages
    // probably overridden by wrapper functions such as connect()
    methodName = 'connectAdvanced',
    // REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
    // calls to render. useful for watching in react devtools for unnecessary re-renders.
    renderCountProp = undefined,
    // determines whether this HOC subscribes to store changes
    shouldHandleStateChanges = true,
    // REMOVED: the key of props/context to get the store
    storeKey = 'store',
    // REMOVED: expose the wrapped component via refs
    withRef = false,
    // use React's forwardRef to expose a ref of the wrapped component
    forwardRef = false,
    // the context consumer to use
    context = ReactReduxContext,
    // additional options are passed through to the selectorFactory
    ...connectOptions
  } = {}
) {
  const Context = context
  // 典型的 HOC 寫法
  return 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
    }
    // 「 prue 」即一種模式,默認爲開啓。影響 Memo 相關內容,能夠視作是一種性能優化
    const { pure } = connectOptions
    const usePureOnlyMemo = pure ? useMemo : callback => callback()
    // 函數組件 ConnectFunction
    function ConnectFunction(props) {
      // props 分類
      const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {
        const { forwardedRef, ...wrapperProps } = props
        return [props.context, forwardedRef, wrapperProps]
      }, [props])
      // 選擇默認 Context 或用戶本身傳入的 Context
      const ContextToUse = useMemo(() => {
        return propsContext &&
          propsContext.Consumer &&
          isContextConsumer(<propsContext.Consumer />)
          ? propsContext
          : Context
      }, [propsContext, Context])

      const contextValue = useContext(ContextToUse)
      // 典型的鴨子模型辨認法
      const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)
      const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)
      const store = didStoreComeFromProps ? props.store : contextValue.store
      // props 選擇器,對應源碼爲 finalPropsSelectorFactory 函數,能夠理解爲一個包裝層
      const childPropsSelector = useMemo(() => {
        return selectorFactory(store.dispatch, selectorFactoryOptions)/* createChildSelector(store) */
      }, [store])
      // 訂閱器及通知函數建立
      const [subscription, notifyNestedSubs] = useMemo(() => {
        if (!shouldHandleStateChanges) return [null, null]/* NO_SUBSCRIPTION_ARRAY */
        // 實例化 Subscription
        const subscription = new Subscription(
          store,
          didStoreComeFromProps ? null : contextValue.subscription
        )
        // 綁定 notifyNestedSubs 的執行上下文環境
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
          subscription
        )
        return [subscription, notifyNestedSubs]
      }, [store, didStoreComeFromProps, contextValue])
      // Determine what {store, subscription} value should be put into nested context, if necessary,
      // and memoize that value to avoid unnecessary context updates.
      const overriddenContextValue = useMemo(() => {
        if (didStoreComeFromProps) {
          return contextValue
        }

        return {
          ...contextValue,
          subscription
        }
      }, [didStoreComeFromProps, contextValue, subscription])

      const lastChildProps = useRef()
      const lastWrapperProps = useRef(wrapperProps)
      const childPropsFromStoreUpdate = useRef()
      const renderIsScheduled = useRef(false)
      // 實際的 props
      const actualChildProps = usePureOnlyMemo(() => {
        if (
          childPropsFromStoreUpdate.current &&
          wrapperProps === lastWrapperProps.current
        ) {
          return childPropsFromStoreUpdate.current
        }
        // props 選擇器,同包裝層
        return childPropsSelector(store.getState(), wrapperProps)
      }, [store, wrapperProps])
      // Hook useEffect 或 useLayoutEffect
      useIsomorphicLayoutEffect(/* captureWrapperProps */() => {
        // 用於更新最新的 props 相關
        lastWrapperProps.current = wrapperProps
        lastChildProps.current = actualChildProps
        renderIsScheduled.current = false
        // 根據條件通知
        if (childPropsFromStoreUpdate.current) {
          childPropsFromStoreUpdate.current = null
          notifyNestedSubs()
        }
      })
      // Our re-subscribe logic only runs when the store/subscription setup changes
      useIsomorphicLayoutEffect(/* subscribeUpdates */ () => {
        if (!shouldHandleStateChanges) return

        let didUnsubscribe = false
        // 檢查更新函數
        const checkForUpdates = () => {
          if (didUnsubscribe) { return }
          const latestStoreState = store.getState()
          // props 選擇器(非包裝層,能夠理解爲 impureFinalPropsSelector 函數,由於省略源碼的關係,重名)
          let newChildProps = childPropsSelector(
            latestStoreState,
            lastWrapperProps.current
          )
          // 若是 props 未變化,則此處無事可作 - 級聯訂閱更新
          if (newChildProps === lastChildProps.current) {
            if (!renderIsScheduled.current) {
              // 通知
              notifyNestedSubs()
            }
          } else {
            // props 變化,觸發通知
            lastChildProps.current = newChildProps
            childPropsFromStoreUpdate.current = newChildProps
            renderIsScheduled.current = true
          }
        }
        // 訂閱 checkForUpdates
        subscription.onStateChange = checkForUpdates
        subscription.trySubscribe()
        // 首次檢查
        checkForUpdates()
        // 取消訂閱函數用於返回
        const unsubscribeWrapper = () => {
          didUnsubscribe = true
          subscription.tryUnsubscribe()
          subscription.onStateChange = null
        }
        return unsubscribeWrapper
      }, [store, subscription, childPropsSelector/* 包裝層 */])

      const renderedWrappedComponent = useMemo(
        () => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
        [forwardedRef, WrappedComponent, actualChildProps]
      )

      const renderedChild = useMemo(() => {
        // 性能優化標示,若爲 false 則不從新渲染
        if (shouldHandleStateChanges) {
          return (
            <ContextToUse.Provider value={overriddenContextValue}>
              {renderedWrappedComponent}
            </ContextToUse.Provider>
          )
        }

        return renderedWrappedComponent
      }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])

      return renderedChild
    }
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName
    // 此處省略含有 React.forwardRef 相關的特殊場景下的代碼內容,若感興趣請移步至 Github 倉庫查看
    // 關於 hoistStatics 請查看: Copies non-react specific statics from a child component to a parent component.
    // Similar to Object.assign, but with React static keywords blacklisted from being overridden.
    return hoistStatics(Connect, WrappedComponent)
  }
}
複製代碼

到這裏, connectAdvanced 高階組件函數的內容已閱讀完,由於篇幅關係,我移除了部分代碼,可能會致使理解不足,就像我在前面說的,這個高階組件的功能就是如此,若你們想深刻建議前往 Github 倉庫查看,大部份內容是一些細節的處理。app

是否是以爲內容有些爆炸,有些多。確實有些,不如,先去喝杯水,上個洗手間,放鬆一下,咱們再繼續。dom

咱們在看到後面的源碼( connect.js )以前須要先看一些內容,還記得 connect 函數執行的時候傳入的參數相似於 mapStateToProps 或者 mapDispatchToProps 的過濾參數,源碼中我會移除這部分內容,因此我在開頭先來舉例說明。

源碼中以 match 函數來對參數進行識別,以條件選項最多的 mapDispatchToProps 爲例子。

即對入參進行以下順序校驗,優先知足即返回:

  1. whenMapDispatchToPropsIsFunction 傳入識別爲函數
  2. whenMapDispatchToPropsIsMissing 未傳入
  3. whenMapDispatchToPropsIsObject 傳入識別爲對象

在瞭解過這個後,咱們來回到源碼:

export function createConnect({ connectHOC = connectAdvanced/* HOC */, 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 } = {} ) {
    // 分別進行入參識別
    const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
    const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
    // 對 connectAdvanced 的封裝
    return connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect',
      // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name => `Connect(${name})`,
      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),
      // passed through to selectorFactory
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,
      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })
  }
}

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

那麼到此,咱們已經看完 connect 函數的內容。

回到前面的內容,整個高階組件函數是爲了實現:

export default connectAdvanced((dispatch, options) => (state, props) => ({
  thing: state.things[props.thingId],
  saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
}))(YourComponent)
複製代碼

相似於這樣的代碼,因此到目前爲止是否是漏了點什麼?是的,被你發現了。是 selectorFactory

export function impureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch ) {
  return function impureFinalPropsSelector(state, ownProps) {
    return mergeProps(
      mapStateToProps(state, ownProps),
      mapDispatchToProps(dispatch, ownProps),
      ownProps
    )
  }
}
複製代碼

我省略了大部分優化代碼,很直觀的理解就是過濾、合併最終你想要的 props 。剩下關於源碼中 Hook 便是一些簡單的封裝,這裏就不作解讀了,很簡單,若是感興趣的話,請移步至 Github 倉庫查看,那麼此次的 React Redux 源碼學習到此,感謝閱讀至此!

相關文章
相關標籤/搜索