react-redux 源碼淺析

react-redux 版本號 7.2.3

react-redux 依賴的庫:html

"dependencies": {
    "@babel/runtime": "^7.12.1",
    "@types/react-redux": "^7.1.16",
    "hoist-non-react-statics": "^3.3.2",
    "loose-envify": "^1.4.0",
    "prop-types": "^15.7.2",
    "react-is": "^16.13.1"
}

這裏我直接把 react-redux 的源碼下載了下來,因此這些依賴就必須手動安裝了react

注意: 關於 hooks 的解析會放到下一文git

redux

redux 是一個庫,但更是一種思想, 而 react-redux 就是一座橋了, 他鏈接了兩中模式, 如今讓咱們一探究竟github

分模塊

咱們將 redux 使用的流程分紅 3 個模塊redux

  1. store 的建立
  2. provider 提供數據的注入
  3. connect 在想要的組件中使用

前置知識點

想要理解此中源碼首先就須要理解不少 react hooks 的知識點 還有熟練使用 redux 的經驗, 這裏我就先簡介一下設計模式

Subscription

咱們要先理解一個設計模式 - 訂閱發佈模式 他位於文件: react-redux/src/utils/Subscription.js 具體的代碼咱們會在後面細說api

hooks

關於 hooks 中 咱們須要瞭解到的知識點:緩存

  • useMemo
    緩存代碼的過程, 若是依賴不變則, 直接返回結果
  • useContext
    在函數中使用 context 的方案
  • useRef
    最開始是用來獲取 ref 的, 後面也用來存儲變量
  • useReducer
    建立一個小的 reducer, 固然也有他本身的 state 和 dispatch

具體的知識點還須要去官網瞭解: https://zh-hans.reactjs.org/d...babel

store

關於 store 的建立併發

store 使用的主要就是 redux 的 api, 無論 combineReducers 仍是 createStore

關於 redux 的 store 提供瞭如下 API:

export interface Store<S = any, A extends Action = AnyAction> {
   
   // dispatch 的動做
  dispatch: Dispatch<A>
    
  // 返回應用程序的當前狀態樹。
  getState(): S

   // 添加更改偵聽器。每當分派動做時,都會調用它,而且狀態樹的某些部分可能已更改。而後,您能夠調用`getState()`來讀取回調中的當前狀態樹。
  subscribe(listener: () => void): Unsubscribe

   // 替換 reducer
  replaceReducer(nextReducer: Reducer<S, A>): void
}

provider

provider 的使用

通常來講, 咱們會在項目入口處加上 Provider, 如 index.js:

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
);

Provider 接受一個 store 做爲存儲, 在 <App/> 中, 任意組件都能獲取到 store 中的參數和方法

此外咱們還能 提供一個 context 給他, 可是通常不建議這樣作, 若是不夠熟悉, 會出現不少未知問題

文件源碼入口

能夠查看文件: react-redux/src/components/Provider.js

//...

// Provider 主體, 是一個組件, 一般在項目的入口使用
function Provider({ store, context, children }) {

  const contextValue = useMemo(() => {
    // 建立了一個訂閱模式, 值爲 store
    // 賦值 onStateChange 爲 notifyNestedSubs,  做用 綁定了 store, 若是 store 值發生了變化 則執行 listener 裏的所並回調
    const subscription = new Subscription(store)
    subscription.onStateChange = subscription.notifyNestedSubs
    return {
      store,
      subscription,
    }
  }, [store])


  // 用來獲取store 的值  記錄,做爲對比
  const previousState = useMemo(() => store.getState(), [store])

  // useIsomorphicLayoutEffect 等於 useLayoutEffect
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue

    // 在 provider 裏面 對於 store 添加 onStateChange 回調, 至關於 subscribe 包裹了一層函數, 這一層的做用後面會體如今 connect 中
    // 除了添加回調  還初始化了 listeners subscribe 事件的機制
    subscription.trySubscribe()

    if (previousState !== store.getState()) {
      // 當知青儲存的值和當前值不一致時  觸發 listeners 裏的回調
      subscription.notifyNestedSubs()
    }
    return () => {
      // 解除事件的監聽
      subscription.tryUnsubscribe()
      subscription.onStateChange = null
    }
  }, [contextValue, previousState])

  // context, 若是外部提供了 則使用外部的 
  const Context = context || ReactReduxContext

  // 就是 context 的 provider
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
// ...

到這裏咱們就碰到了 Subscription 了, 如今須要知道的兩點:

  1. 經過 subscription.addNestedSub(listener) 函數, 添加監聽事件
  2. 經過 subscription.notifyNestedSubs(), 觸發以前全部的監聽事件
  3. subscription.trySubscribe() 屬於第一點中函數的子函數, 效果出來不能添加回調之外,相似
  4. subscription.tryUnsubscribe(), 與第三點相反, 解除監聽

如今, 咱們羅列下 Provider 作了什麼事情:

  1. 建立了 context 須要傳遞的值
  2. 記錄以前 store 的值
  3. 當前 store 值和以前記錄的不同時, 觸發監聽事件

connect

真正的重頭戲來了, 將 redux 的 store 與任意的組件鏈接

connect 的使用

connect 參數

在這裏咱們首先須要知道的是 connect , 經過他是怎麼使用的, 倒推回去看源碼會更有幫助 他的定義:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

能夠看到connect 可接受 4 個參數

  1. mapStateToProps:
mapStateToProps?: (state, ownProps?) => Object

他是一個函數, 接受 state 和 ownProps 兩個參數, 返回一個對象, 若是 mapStateToProps 傳遞的是一個函數, 那麼 store 更新的時候,包裝的組件也會訂閱更新 若是傳遞 undefined 或者
null, 能夠避免不須要的更新

關於 ownProps 的用法, ownProps 其實就是組件的 props

const mapStateToProps = (state, ownProps) => ({
  todo: state.todos[ownProps.id],
})
  1. mapDispatchToProps
mapDispatchToProps?: Object | (dispatch, ownProps?) => Object

第二個參數, 能夠是函數, 能夠是對象, 也能夠是空值 若是是函數, 則能夠收取到兩個參數, dispatchownProps
一般咱們是這樣作的:

const mapDispatchToProps = (dispatch) => {
return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
  }
}

ownProps 的用法和 mapStateToProps 相同 當前參數若是是一個對象的時候,
須要控制裏面的屬性都是 action-creator
在源碼中將會調用: bindActionCreators(mapDispatchToProps, dispatch) 來生成可用代碼
官網中的簡介: 點擊查看

  1. mergeProps
mergeProps?: (stateProps, dispatchProps, ownProps) => Object

這個參數的做用就是, 當前 connect 包裝的組件, 對於他的 props 再次自定義 ,如不傳遞這個屬性, 則代碼中默認傳遞值爲: { ...ownProps, ...stateProps, ...dispatchProps }

  1. options
options?: Object

Object 中的內容:

{
  context?: Object,
  pure?: boolean,
  areStatesEqual?: Function,
  areOwnPropsEqual?: Function,
  areStatePropsEqual?: Function,
  areMergedPropsEqual?: Function,
  forwardRef?: boolean,
}

只有版本再 >=6.0 的時候纔會有這個屬性, 都是配置性的屬性, 通常來講默認值就能應付 99% 的狀況了
更加具體的做用能夠在此處點擊查看: 點擊查看

connect 返回結果:

這是一個普通的用法:

connect(mapStateToProps, mapDispatchToProps)(App);

不難理解, connect 做爲一個高階函數, 返回的也是一個函數, 因此纔會是這種用法

const connect = (mapStateToProps, mapDispatchToProps)=>{
  return (Component) => {
    return  <Conponent /> 
  }
}

具體應該就是這樣, 如今帶着咱們的理解和疑問再來進入 connect 源碼

文件源碼入口:

查看 connect 的入口文件 src/connect/connect :
這個文件定義了一個 createConnect 函數, 這是用來生成 connect 的:

export function createConnect({
                                  connectHOC = connectAdvanced,
                                  mapStateToPropsFactories = defaultMapStateToPropsFactories,
                                  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
                                  mergePropsFactories = defaultMergePropsFactories,
                                  selectorFactory = defaultSelectorFactory,
                              } = {}) {
    // 返回真正的 connect 函數
    return function connect(
        mapStateToProps,
        mapDispatchToProps,
        mergeProps,
        {
            pure = true,
            areStatesEqual = strictEqual,
            areOwnPropsEqual = shallowEqual,
            areStatePropsEqual = shallowEqual,
            areMergedPropsEqual = shallowEqual,
            ...extraOptions
        } = {}
    ) {

        // 判斷 mapStateToProps 是否符合已經定義的規則
        // mapStateToPropsFactories 能夠想象成你對
        // mapStateToProps 作了一些判斷, 只要有一個判斷符合了
        // 就能夠成功返回值
        // mapStateToPropsFactories 的規則會在 react-redux/src/connect/mapStateToProps.js 裏講解
        // 默認的 defaultMapStateToPropsFactories 有兩個規則
        // 1. 若是是函數, 會使用 wrapMapToPropsFunc 包裹, 而且直接return結果
        // 2. 若是沒有傳值, 則會使用 wrapMapToPropsConstant 包裹
        const initMapStateToProps = match(
            mapStateToProps,
            mapStateToPropsFactories,
            'mapStateToProps'
        )

        // 同上 可是 他的默認規則是 defaultMapDispatchToPropsFactories
        // 在 react-redux/src/connect/mapDispatchToProps.js 此文件中
        const initMapDispatchToProps = match(
            mapDispatchToProps,
            mapDispatchToPropsFactories,
            'mapDispatchToProps'
        )

        // 同上
        const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

        // 包裹組件的高階函數 connect(mapStateToProps, ...)
        return connectHOC(selectorFactory, {
            // 方便 error messages 打印
            methodName: 'connect',

            // 用於從包裝的組件的displayName計算Connect的displayName。
            getDisplayName: (name) => `Connect(${name})`,

            // 若是mapStateToProps 爲 falsy,則Connect組件不訂閱存儲狀態更改
            shouldHandleStateChanges: Boolean(mapStateToProps),

            //  傳遞給 selectorFactory 的參數
            initMapStateToProps,
            initMapDispatchToProps,
            initMergeProps,
            pure,
            areStatesEqual,
            areOwnPropsEqual,
            areStatePropsEqual,
            areMergedPropsEqual,
            //
            ...extraOptions,
        })
    }
}

defaultMapStateToPropsFactories, defaultMapDispatchToPropsFactories , defaultMergePropsFactories
, defaultSelectorFactory 咱們會放在下面研究, 如今先知道他是作什麼的

一樣的, 在這個文件 咱們能夠看到 connect 的雛形了

真實的執行順序: createConnect() -> connect() -> connectHOC() = connectAdvanced() -> wrapWithConnect(Component)

下一步就是 connectAdvanced() 中執行了什麼:

connectAdvanced

這個咱們須要在 react-redux/src/components/connectAdvanced.js 這個文件中查看:

connectAdvanced 較爲複雜, 咱們將它分段提取, 首先咱們來看他的傳參

export default function connectAdvanced(
    // 這些是 connect 第一步中提供的參數
    selectorFactory, // 默認爲 defaultSelectorFactory
    // options object:
    {
        //用於從包裝的組件的displayName計算此HOC的displayName的函數。
        getDisplayName = (name) => `ConnectAdvanced(${name})`,

        // 在 error message  中顯示 容易 debug
        methodName = 'connectAdvanced',

        // 沒有太大做用, 後面可能會刪除
        renderCountProp = undefined,

        // 肯定這個 hoc 是否會監聽 store 的改變
        shouldHandleStateChanges = true,

        // 沒有太大做用, 後面可能會刪除

        storeKey = 'store',
        // 沒有太大做用, 後面可能會刪除
        withRef = false,

        // 是否使用了 forwardRef
        forwardRef = false,

        // 使用的 Context
        context = ReactReduxContext,

        //額外參數
        ...connectOptions
    } = {}
) {
    // 省略了參數的校驗
    const Context = context
    return function wrapWithConnect(WrappedComponent) {
        // ...
    }
}

這些參數都是能夠在 createConnect 中找到, 能夠看到 connectAdvanced 返回的 wrapWithConnect, 就是咱們用來正真返回的, 用來包裹組件的函數

wrapWithConnect

wrapWithConnect 也有一個主體函數 ConnectFunction, 這裏咱們先講除此函數以外的做用

function wrapWithConnect(WrappedComponent) {
    // 省略校檢

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

    const displayName = getDisplayName(wrappedComponentName)
    // 上面兩行都是獲取組件名稱 默認(Component)


    const selectorFactoryOptions = {
        ...connectOptions,
        getDisplayName,
        methodName,
        renderCountProp,
        shouldHandleStateChanges,
        storeKey,
        displayName,
        wrappedComponentName,
        WrappedComponent,
    }
    // 將 WrappedComponent 和  connectAdvanced中的參數集合在了一塊兒

    const {pure} = connectOptions // 第一步傳遞過來的參數  默認爲 true

    // 建立子選擇器的函數 聲明
    function createChildSelector(store) {
        return selectorFactory(store.dispatch, selectorFactoryOptions)
    }


    // 若是 pure 爲 false, 則直接指向回調 而不是 useMemo
    const usePureOnlyMemo = pure ? useMemo : (callback) => callback()

    // 當前整個函數的主體部分 接受 props 返回 JSX 而且會用 Context 包裹
    function ConnectFunction(props) {
        // 省略函數主體
    }

    // 經過 pure 來肯定是否要加 memo
    const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction

    Connect.WrappedComponent = WrappedComponent
    Connect.displayName = displayName

    // forwardRef 省略

    return hoistStatics(Connect, WrappedComponent)
}

hoistStatics

這裏要說下 hoistStatics , 他來自於 hoist-non-react-statics 這個庫
簡單的來講能夠當作 Object.assign, 可是他是組件級別的

ConnectFunction

ConnectFunction 能夠說是通過上一步的包裝以後 真正在執行中的函數

function ConnectFunction(props) {
    const [
        propsContext,
        reactReduxForwardedRef,
        wrapperProps,
    ] = useMemo(() => {
        // 區分傳遞給包裝器組件的實際「數據」屬性和控制行爲所需的值(轉發的引用,備用上下文實例)。
        // 要維護wrapperProps對象引用,緩存此解構。
        // 此處使用的是官方註釋
        const {reactReduxForwardedRef, ...wrapperProps} = props
        return [props.context, reactReduxForwardedRef, wrapperProps]
    }, [props])

    const ContextToUse = useMemo(() => {
        // 用戶能夠選擇傳入自定義上下文實例來代替咱們的ReactReduxContext使用。
        // 記住肯定應該使用哪一個上下文實例的檢查。
        // 此處使用的是官方註釋
        return propsContext &&
        propsContext.Consumer &&
        isContextConsumer(<propsContext.Consumer/>)
            ? propsContext
            : Context
    }, [propsContext, Context])

    // useContext 不用多說
    const contextValue = useContext(ContextToUse)
    // 到此處位置都是 context 的預備工做


    // store 必須存在於 props 或 context
    // 咱們將首先檢查它是否看起來像 Redux store。
    // 這使咱們能夠經過一個 「store」 props,該 props 只是一個簡單的值。
    const didStoreComeFromProps =
        Boolean(props.store) &&
        Boolean(props.store.getState) &&
        Boolean(props.store.dispatch)

    // 確認 store 是否來自於本地 context
    const didStoreComeFromContext =
        Boolean(contextValue) && Boolean(contextValue.store)

    //省略報錯判斷

    // 獲取 store  賦值
    const store = didStoreComeFromProps ? props.store : contextValue.store
    // 到這是 store 的判斷


    const childPropsSelector = useMemo(() => {
        // 子道具選擇器須要store參考做爲輸入。每當store更改時,則從新建立此選擇器。
        return createChildSelector(store)
    }, [store])

    const [subscription, notifyNestedSubs] = useMemo(() => {
        // 肯定這個 hoc 是否會監聽 store 的改變 默認爲 true
        if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY // [null, null]

        // new 一個新的 Subscription
        // 此訂閱的來源應與存儲來自何處相匹配:store vs context。
        // 經過 props 鏈接到 store 的組件不該使用 context 訂閱,反之亦然。
        // 文件來源 react-redux/src/utils/Subscription.js
        const subscription = new Subscription(
            store,
            didStoreComeFromProps ? null : contextValue.subscription
        )

        // `notifyNestedSubs`是重複的,以處理組件在通知循環中間被取消訂閱的狀況,
        // 此時`subscription`將爲空。 若是修改Subscription的監聽器邏輯,
        // 不在通知循環中間調用已取消訂閱的監聽器,就能夠避免這種狀況。
        // 此處使用的是官方註釋
        const notifyNestedSubs = subscription.notifyNestedSubs.bind(
            subscription
        )

        return [subscription, notifyNestedSubs]
    }, [store, didStoreComeFromProps, contextValue])

    // 若是須要的話,肯定應該把什麼{store,subscription}值放到嵌套的context中
    // ,並將該值備忘,以免沒必要要的上下文更新。
    const overriddenContextValue = useMemo(() => {
        if (didStoreComeFromProps) {
            // 這個組件是直接從props訂閱一個存儲.
            // 咱們不但願子孫從這個存儲中讀取--不管現有的上下文值是來自最近的鏈接祖先的什麼,
            // 都會傳下來。
            return contextValue
        }

        // 不然,把這個組件的訂閱實例放到上下文中,這樣鏈接的子孫就不會更新,直到這個組件完成以後。
        return {
            ...contextValue,
            subscription,
        }
    }, [didStoreComeFromProps, contextValue, subscription])

    // 每當 Redux store 更新致使計算出的子組件 props 發生變化時,咱們須要強制這個包裝組件從新渲染(或者咱們在mapState中發現了一個錯誤)。
    const [
        [previousStateUpdateResult],
        forceComponentUpdateDispatch,
    ] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)

    // 拋出任何 mapState/mapDispatch 錯誤。
    if (previousStateUpdateResult && previousStateUpdateResult.error) {
        throw previousStateUpdateResult.error
    }

    // 設置 ref,以協調訂閱效果和渲染邏輯之間的數值。
    // 參考 經過 ref 能夠獲取,存儲值
    const lastChildProps = useRef()
    const lastWrapperProps = useRef(wrapperProps)
    const childPropsFromStoreUpdate = useRef()
    const renderIsScheduled = useRef(false)

    const actualChildProps = usePureOnlyMemo(() => {
        // 這裏的邏輯很複雜:
        // 這個渲染多是由 Redux store 更新所觸發,產生了新的子 props。
        // 不過,在那以後,咱們可能會獲得新的包裝 props。
        // 若是咱們有新的子 props ,和相同的包裝 props , 咱們知道咱們應該按原樣使用新的子 props .
        // 可是,若是咱們有新的包裝props,這些可能會改變子 props ,因此咱們必須從新計算這些.
        // 因此,只有當包裝 props 和上次同樣時,咱們纔會使用 store 更新的子 props。
        if (
            childPropsFromStoreUpdate.current &&
            wrapperProps === lastWrapperProps.current
        ) {
            return childPropsFromStoreUpdate.current
        }

        // 這極可能會致使在併發模式下發生壞事(TM)。
        // 請注意,咱們之因此這樣作是由於在由存儲更新引發的渲染中,
        // 咱們須要最新的存儲狀態來肯定子 props 應該是什麼。
        return childPropsSelector(store.getState(), wrapperProps)
    }, [store, previousStateUpdateResult, wrapperProps])

    // 咱們須要在每次從新渲染時同步執行。
    // 然而,React會對SSR中的useLayoutEffect發出警告, 避免警告
    // 至關於在 useLayoutEffect 中執行, 包裝了一下
    // 第一個參數是待執行函數, 第二個是函數參數, 第三個依賴
    useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        wrapperProps,
        actualChildProps,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
    ])

    // 咱們的從新訂閱邏輯只有在 store或者訂閱設置發生變化時纔會運行。
    useIsomorphicLayoutEffectWithArgs(
        subscribeUpdates,
        [
            shouldHandleStateChanges,
            store,
            subscription,
            childPropsSelector,
            lastWrapperProps,
            lastChildProps,
            renderIsScheduled,
            childPropsFromStoreUpdate,
            notifyNestedSubs,
            forceComponentUpdateDispatch,
        ],
        [store, subscription, childPropsSelector]
    )

    // 如今全部這些都完成了,咱們終於能夠嘗試實際渲染子組件了。
    // 咱們將渲染後的子組件的元素進行記憶,做爲一種優化。
    const renderedWrappedComponent = useMemo(
        () => (
            <WrappedComponent
                {...actualChildProps}
                ref={reactReduxForwardedRef}
            />
        ),
        [reactReduxForwardedRef, WrappedComponent, actualChildProps]
    )

    // 若是React看到了與上次徹底相同的元素引用,它就會退出從新渲染該子元素,就像在React.memo()中被包裹或從shouldComponentUpdate中返回false同樣。
    const renderedChild = useMemo(() => {

        // 肯定這個 hoc 是否會監聽 store 的改變, 默認是 true
        if (shouldHandleStateChanges) {
            // 若是這個組件訂閱了存儲更新,咱們須要將它本身的訂閱實例傳遞給咱們的子孫。
            // 這意味着渲染相同的Context實例,並將不一樣的值放入context中。
            return (
                <ContextToUse.Provider value={overriddenContextValue}>
                    {renderedWrappedComponent}
                </ContextToUse.Provider>
            )
        }

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

    return renderedChild
}

這一部分即是 connect 的核心代碼

再肢解一下上面的代碼可分爲一下幾個步驟:

肯定 Context -> 肯定 store 來源 -> 將一個訂閱,發佈合併到 contextValue 中 -> 組件更新後, 檢查 store 值是否變化 -> 返回包裝組件

再解釋這部分代碼中的引用的部分函數: captureWrapperProps , subscribeUpdates

captureWrapperProps

代碼是在這裏:

useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs,
])

轉換一下:

useLayoutEffect(() => {
    captureWrapperProps(
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        wrapperProps,
        actualChildProps,
        childPropsFromStoreUpdate,
        notifyNestedSubs
    )
})

function captureWrapperProps(
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    wrapperProps,
    actualChildProps,
    childPropsFromStoreUpdate,
    notifyNestedSubs
) {
    lastWrapperProps.current = wrapperProps
    lastChildProps.current = actualChildProps
    renderIsScheduled.current = false

    // 若是渲染是來自store的更新,則清除該引用 而且觸發訂閱
    if (childPropsFromStoreUpdate.current) {
        childPropsFromStoreUpdate.current = null
        notifyNestedSubs()
    }
}

subscribeUpdates

源碼:

useIsomorphicLayoutEffectWithArgs(
    subscribeUpdates,
    [
        shouldHandleStateChanges,
        store,
        subscription,
        childPropsSelector,
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
        forceComponentUpdateDispatch,
    ],
    [store, subscription, childPropsSelector]
)

一樣地通過轉換:

useLayoutEffect(() => {
    subscribeUpdates(
        shouldHandleStateChanges,
        store,
        subscription,
        childPropsSelector,
        lastWrapperProps,
        lastChildProps,
        renderIsScheduled,
        childPropsFromStoreUpdate,
        notifyNestedSubs,
        forceComponentUpdateDispatch,
    )
}, [store, subscription, childPropsSelector])

咱們再看 subscribeUpdates 作了什麼, 這裏就比較複雜了:

function subscribeUpdates(
    shouldHandleStateChanges,
    store,
    subscription,
    childPropsSelector,
    lastWrapperProps,
    lastChildProps,
    renderIsScheduled,
    childPropsFromStoreUpdate,
    notifyNestedSubs,
    forceComponentUpdateDispatch
) {
    // 若是不想從 store 中更新, 則直接返回
    if (!shouldHandleStateChanges) return

    let didUnsubscribe = false
    let lastThrownError = null

    // 每次 store 的訂閱更新傳播到這個組件時,咱們都會運行這個回調。
    const checkForUpdates = () => {
        if (didUnsubscribe) {
            // Redux不能保證取消訂閱會在下一次發送以前發生。
            return
        }

        const latestStoreState = store.getState()

        let newChildProps, error
        try {
            // 用最新的store狀態和包裝運行選擇器
            newChildProps = childPropsSelector(
                latestStoreState,
                lastWrapperProps.current
            )
        } catch (e) {
            error = e
            lastThrownError = e
        }

        if (!error) {
            lastThrownError = null
        }

        // 若是沒變化就不作什麼
        if (newChildProps === lastChildProps.current) {
            if (!renderIsScheduled.current) {
                notifyNestedSubs()
            }
        } else {
            // 保存對新的子props的引用。 
            lastChildProps.current = newChildProps
            childPropsFromStoreUpdate.current = newChildProps
            renderIsScheduled.current = true

            // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
            // 若是 子 props 確實發生了變化, 那麼  wrapperComponent 須要重渲染
            forceComponentUpdateDispatch({
                type: 'STORE_UPDATED',
                payload: {
                    error,
                },
            })
        }
    }

    subscription.onStateChange = checkForUpdates
    subscription.trySubscribe()

    // 執行 
    checkForUpdates()

    // 在第一次渲染後從store拉出數據,以防store在咱們開始後發生變化。
    const unsubscribeWrapper = () => {
        didUnsubscribe = true
        subscription.tryUnsubscribe()
        subscription.onStateChange = null


        // 若是出錯, 可是到此聲明週期還沒解決, 就觸發報錯
        if (lastThrownError) {
            throw lastThrownError
        }
    }

    return unsubscribeWrapper
}

從這幾行能夠看出來, store 或者 props 的變化都會致使此包裝組件的再渲染, 選渲染中又加上了判斷, 能夠控制子組件是否真的可以渲染

補漏

selectorFactory

這函數是獲取 store 的, 以前使用的地方

// childPropsSelector使用 1
newChildProps = childPropsSelector(
        latestStoreState,
        lastWrapperProps.current
)
// childPropsSelector使用 2
childPropsSelector(store.getState(), wrapperProps)

const childPropsSelector = useMemo(() => {
    return createChildSelector(store)
}, [store])

function createChildSelector(store) {
    return selectorFactory(store.dispatch, selectorFactoryOptions)
}

在默認狀況下 selectorFactory = defaultSelectorFactory
源文件: react-redux/src/connect/selectorFactory.js

defaultSelectorFactory 別名: finalPropsSelectorFactory

// 若是pure爲true,則selectorFactory返回的選擇器將記住其結果,
// 若是未更改結果,則connectAdvanced的shouldComponentUpdate能夠返回false。
// 若是爲false,則選擇器將始終返回新對象,而shouldComponentUpdate將始終返回true。

// 默認的選擇器工廠
export default function finalPropsSelectorFactory(
  dispatch,
  { initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {

  // initMapStateToProps 可在 connect 中查看 就是經過 match 獲取的結果
  const mapStateToProps = initMapStateToProps(dispatch, options)
  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
  const mergeProps = initMergeProps(dispatch, options)

  // 忽略驗證

  const selectorFactory = options.pure
    ? pureFinalPropsSelectorFactory
    : impureFinalPropsSelectorFactory

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

這裏再次進行到下一流程 initMapStateToProps , initMapDispatchToProps , initMergeProps:
這三個變量的來源是在這裏:

const initMapStateToProps = match(
  mapStateToProps,
  mapStateToPropsFactories,
  'mapStateToProps'
)

const initMapDispatchToProps = match(
  mapDispatchToProps,
  mapDispatchToPropsFactories,
  'mapDispatchToProps'
)

const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')

而這, 咱們又接觸到了新的幾個參數 match, mapStateToPropsFactories ,mapDispatchToPropsFactories ,mergePropsFactories:

match

先說說 match, 來看看源碼:

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) => {
    throw new Error(
      `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${
        options.wrappedComponentName
      }.`
    )
  }
}

這個的做用,咱們以前也略微講過, 就是經過執行 factories 中的函數, 若是有返回值則返回對應的值

而這裏咱們也須要說下這幾個 Factories: defaultMapStateToPropsFactories, defaultMapDispatchToPropsFactories, defaultMergePropsFactories

defaultMapStateToPropsFactories

export function whenMapStateToPropsIsFunction(mapStateToProps) {
  return typeof mapStateToProps === 'function'
    ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
    : undefined
}

export function whenMapStateToPropsIsMissing(mapStateToProps) {
  return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
}

export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]

通過以前咱們的 connect 用法的介紹:

mapStateToProps?: (state, ownProps?) => Object

若是 mapStateToProps 傳的是一個函數, 則用 wrapMapToPropsFunc 包裹, 否則就包裹一個空函數

咱們再來看下 wrapMapToPropsFunc:

export function wrapMapToPropsFunc(mapToProps, methodName) {
  return function initProxySelector(dispatch, { displayName }) {
    const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
      return proxy.dependsOnOwnProps
        ? proxy.mapToProps(stateOrDispatch, ownProps)
        : proxy.mapToProps(stateOrDispatch)
    }

    proxy.dependsOnOwnProps = true

    proxy.mapToProps = function detectFactoryAndVerify(
      stateOrDispatch,
      ownProps
    ) {
      proxy.mapToProps = mapToProps
      proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
      let props = proxy(stateOrDispatch, ownProps)

      if (typeof props === 'function') {
        proxy.mapToProps = props
        proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
        props = proxy(stateOrDispatch, ownProps)
      }

      // 註釋驗證
      
      return props
    }

    return proxy
  }
}

// dependsOnOwnProps默認爲 true
// 判斷 mapToProps 的dependsOnOwnProps屬性是否爲空,
// 若是不爲空則, 則返回 Boolean(dependsOnOwnProps), 若是爲空, 則比較後返回 布爾值
function getDependsOnOwnProps(mapToProps) {
  return mapToProps.dependsOnOwnProps !== null &&
  mapToProps.dependsOnOwnProps !== undefined
          ? Boolean(mapToProps.dependsOnOwnProps)
          : mapToProps.length !== 1
}

經歷過 match 的遍歷, 返回的就是 initProxySelector, 這個地方設計得很巧妙
initProxySelector 的時候, 傳入值: dispatch, options, 這裏 options 能夠暫時忽略 , 這裏是有mapToProps的入參
他的返回結果也是一個函數, 即 proxy 函數

proxy 執行:

執行的是 proxy.mapToProps(stateOrDispatch, ownProps)detectFactoryAndVerify
覆蓋原 mapToProps: proxy.mapToProps = mapToProps 這裏覆蓋的就是咱們傳入的 mapStateToProps 函數 / 或者 undefined,
proxy.dependsOnOwnProps 正常狀況下都是返回 true
這時候 再次執行 proxy: let props = proxy(stateOrDispatch, ownProps)
轉換一下: mapToProps(stateOrDispatch, ownProps), 這裏的 mapToProps 是咱們傳入的,
以後繼續往下走:

if (typeof props === 'function') {
  proxy.mapToProps = props
  proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
  props = proxy(stateOrDispatch, ownProps)
}

這裏是對於返回結果又作了一層判斷, 若是返回的是一個函數, 將會覆蓋
由於這個判斷, 因此咱們能夠這樣傳 mapStateToProps:

const mapStateToProps = (_state) => {
  return (state) => ({
    value: state.value
  })
}

因此說查看源碼, 是能夠發現一些新的用法的, 雖然這樣的寫法不是很提倡, 暫時沒什麼做用, 但爲後面他的拓展性作了很充足的準備

defaultMapDispatchToPropsFactories

這個參數是和 defaultMapStateToPropsFactories 相似了
並且由於 mapDispatchToProps 能夠傳入 Object, 在 match 上又會多一層判斷
這裏涉及到的代碼和 defaultMapStateToPropsFactories 90% 都是相似了, 因此跳過

defaultMergePropsFactories

同上, 基本相似, 也再也不贅述

selectorFactory

到這裏繼續講 selectorFactory, 通常來講 selectorFactory 運行的都是此函數 pureFinalPropsSelectorFactory,
代碼一樣是在 react-redux/src/connect/selectorFactory.js 此文件夾下的
入參:

mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  dispatch,
  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }

在此函數體內, 他又定義了幾個函數, 最關鍵是這個:

function handleSubsequentCalls(nextState, nextOwnProps) {
  const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
  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) {
    // 表示是否已經運行過一次, 若是沒有,則使用 handleFirstCall 初始化
    // 不然使用 handleSubsequentCalls
  return hasRunAtLeastOnce
          ? handleSubsequentCalls(nextState, nextOwnProps)
          : handleFirstCall(nextState, nextOwnProps)
}

這裏的比較方法areOwnPropsEqual,areStatesEqual 默認都是 shallowEqual
(來自文件: react-redux/src/utils/shallowEqual.js), 算是一個淺層比較
經過比較 新舊 props 和 state, 若是發生變化, 則進行對應的更新最終返回合併後的值

總結下流程:

咱們傳遞的 mapStateToProps ->
通過 match 函數 ->
match 函數中的 wrapMapToPropsFunc ->
如今執行的是 initProxySelector ->
別名 initMapStateToProps ->
經過執行他 得到結果 const mapStateToProps = initMapStateToProps(dispatch, options) ->
經過 finalPropsSelectorFactory 的包裝 ->
別名 selectorFactory ->
在函數中杯執行 selectorFactory(store.dispatch, selectorFactoryOptions) ->
返回的值, 做爲 childPropsSelector 的值 ->
在新舊 props 比較時(subscribeUpdates中)使對此這個值

結語

本文較爲簡單的解析了一下 react-redux 這個經常使用庫, 總結了幾種代碼運行流程

  1. store 數據流向
    store -> Provider -> connect -> props
  2. connect 執行函數流程
    createConnect() -> connect() -> connectHOC() = connectAdvanced() -> wrapWithConnect(Component)
  3. mapStateToProps執行流程
    咱們傳遞的 mapStateToProps ->
    通過 match 函數 ->
    match 函數中的 wrapMapToPropsFunc ->
    如今執行的是 initProxySelector ->
    別名 initMapStateToProps ->
    經過執行他 得到結果 const mapStateToProps = initMapStateToProps(dispatch, options) ->
    經過 finalPropsSelectorFactory 的包裝 ->
    別名 selectorFactory ->
    在函數中杯執行 selectorFactory(store.dispatch, selectorFactoryOptions) ->
    返回的值, 做爲 childPropsSelector 的值 ->
    在新舊 props 比較時(subscribeUpdates中)使對此這個值

本文記錄:
https://github.com/Grewer/rea...

參考文檔:
https://react-redux.js.org/in...
https://github.com/reduxjs/re...

相關文章
相關標籤/搜索