阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理
- 閱讀react-redux源碼 - 零
- 閱讀react-redux源碼 - 一
- 閱讀react-redux源碼(二) - createConnect、match函數的實現
- 閱讀react-redux源碼(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 閱讀react-redux源碼(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
- 閱讀react-redux源碼(五) - connectAdvanced中store改變的事件轉發、ref的處理和pure模式的處理
在閱讀react-redux源碼(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates中介紹了函數connectAdvanced是如何關聯到store變動的,其實這個函數做的事情不僅僅有關聯變動,還有對于store改變的事件轉發、ref的處理和pure模式的處理。
首先來看store改變的事件的轉發。
store改變的事件轉發
function connectAdvanced (selectorFactory,{...context = ReactReduxContext,...} ) {...const Context = context...return function wrapWithConnect (WrappedComponent) {...function ConnectFunction (props) {const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {// Distinguish between actual "data" props that were passed to the wrapper component,// and values needed to control behavior (forwarded refs, alternate context instances).// To maintain the wrapperProps object reference, memoize this destructuring.const { forwardedRef, ...wrapperProps } = propsreturn [props.context, forwardedRef, wrapperProps]}, [props])const ContextToUse = useMemo(() => {// 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])// Retrieve the store and ancestor subscription via context, if availableconst contextValue = useContext(ContextToUse)// The store _must_ exist as either a prop or in context.// We'll check to see if it _looks_ like a Redux store first.// This allows us to pass through a `store` prop that is just a plain value.const didStoreComeFromProps =Boolean(props.store) &&Boolean(props.store.getState) &&Boolean(props.store.dispatch)const didStoreComeFromContext =Boolean(contextValue) && Boolean(contextValue.store)if (process.env.NODE_ENV !== 'production' &&!didStoreComeFromProps &&!didStoreComeFromContext) {throw new Error(`Could not find "store" in the context of ` +`"${displayName}". Either wrap the root component in a <Provider>, ` +`or pass a custom React context provider to <Provider> and the corresponding ` +`React context consumer to ${displayName} in connect options.`)}// Based on the previous check, one of these must be trueconst store = didStoreComeFromProps ? props.store : contextValue.storeconst [subscription, notifyNestedSubs] = useMemo(() => {if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY// This Subscription's source should match where store came from: props vs. context. A component// connected to the store via props shouldn't use subscription from context, or vice versa.const subscription = new Subscription(store,didStoreComeFromProps ? null : contextValue.subscription)// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in// the middle of the notification loop, where `subscription` will then be null. This can// probably be avoided if Subscription's listeners logic is changed to not call listeners// that have been unsubscribed in the middle of the notification loop.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) {// This component is directly subscribed to a store from props.// We don't want descendants reading from this store - pass down whatever// the existing context value is from the nearest connected ancestor.return contextValue}// Otherwise, put this component's subscription instance into context, so that// connected descendants won't update until after this component is donereturn {...contextValue,subscription}}, [didStoreComeFromProps, contextValue, subscription])}...// If React sees the exact same element reference as last time, it bails out of re-rendering// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.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.return (<ContextToUse.Provider value={overriddenContextValue}>{renderedWrappedComponent}</ContextToUse.Provider>)}return renderedWrappedComponent}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])return renderedChild}const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction...return hoistStatics(Connect, WrappedComponent)}可以看出來ConnectFunction組件中如果props傳入了合法的context那么ContextToUse就是props.context如果不是則使用默認的ReactReduxContext。
到這里拿到了ContextToUse,后面通過useContext(ContextToUse)得到context中傳過來的值contextValue。通過前面的解讀我們知道contextValue需要一個對象中包含兩個值,一個是store一個是subscription(Subscription的實例)。
除了context可以使用來自props外store也可以來自props。如果props中有store那么就放棄contextValue中的store而使用props中傳入的store。
如果contextValue中也沒有store,畢竟contextValue可能來自用戶傳入,props中也沒有store,沒有store監聽啥,繼續不下去了,直接報錯。
根據store和contextValue和didStoreComeFromProps三個值來生成新的subscription(不明白為啥要生成新的,直接訂閱傳入的subscription不是更加簡單嗎?如果是props.store再生成新的)。得到新的subscription和subscription的通知函數notifyNestedSubs,來通知subscription的訂閱者。
重寫contextValue使用新生成的subscription來覆蓋contextValue中的subscription,然后通過Provider提供給后代組件。
小結:整個react-redux構建的上下文中,并不只能有位移的store和context,還可以通過props的方式傳入別的store和context讓Connect組件響應這個傳入的store和context的變動。
獲取業務組件的實例(透傳ref)
function createConnect() {return function connect() {return function connectHOC()} }export default createConnect() // 就是上面的connectHOC function connectAdvanced () {return function wrapWithConnect(WrappedComponent) {function ConnectFunction () {return <WrappedComponent />}return ConnectFunction} }基本上connect(mapStateToProps, mapDispatchToProps)(wrappedComponent)就是這個結構,所以最后被返回的出去的是ConnectFunction這個組件,如果在connectFunction上設置props ref也拿不到業務組件WrappedComponent的實例。
首先在connectAdvanced的配置項中有forwardRef = false如果是true,那么connectAdvanced就會幫你透傳ref給業務組件。
看看如何做的:
function connectAdvanced(selectorFactory,{forwardRef = false} ) {return function wrapWithConnect(WrappedComponent) {function ConnectFunction () {const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {const { forwardedRef, ...wrapperProps } = propsreturn [forwardedRef, wrapperProps]}, [props])return <WrappedComponent ref={forwardedRef} />}return ConnectFunctionif (forwardRef) {const forwarded = React.forwardRef(function forwardConnectRef(props,ref) {return <ConnectFunction {...props} forwardedRef={ref} />})return forwarded}return ConnectFunction} }上面的代碼基本上就是透傳ref的所有實現了,和源碼有些出入,主要是刪除了一些干擾因素。
如果配置項中forwardRef是true那么wrapWithConnect返回的就是:
React.forwardRef(function forwardConnectRef(props, ref) {return <ConnectFunction {...props} forwardedRef={ref} />} )使用React.forwardRef提取了ref通過keyforwardedRef屬性傳遞給了ConnectFunction組件。connectFunction組件內部拿到forwardedRef只有再給到業務組件<ConnectFunction ref={forwardedRef} />。這樣就完成了透傳ref的任務,讓父組件可以拿到被包裹的業務組件的實例。
pure模式
對于PureComponent我們有所了解,當props變動的時候這個Component會幫助我們自動做一個props的淺比較,如果淺比較相等則不會update組件,如果不相等才會update組件。
對于類組件可以繼承PureComponent而函數式組件可以使用React.memo來做同樣的事情,當然鉤子函數useMemo也可以達到同樣的效果。
業務組件的props有兩個來源,一個是父組件,一個是store。store又被分為兩個,一個是store的state,一個是store的dispatch。
想要讓整個業務組件是pure那么就要考慮兩個方面的變動,一個是父組件重新渲染的時候需要淺比較當前業務子組件的props來阻止不必要的更新,另一個是store中state更新的時候,需要對當前組件監聽的state做淺比較防止不必要的更新。
return function wrapWithConnect(WrappedComponent) {...const { pure } = connectOptions...const usePureOnlyMemo = pure ? useMemo : callback => callback()...function ConnectFunction(props) {...const actualChildProps = usePureOnlyMemo(() => {if (childPropsFromStoreUpdate.current &&wrapperProps === lastWrapperProps.current) {return childPropsFromStoreUpdate.current}return childPropsSelector(store.getState(), wrapperProps)}, [store, previousStateUpdateResult, wrapperProps])...}...const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction... }首先拿到配置中的pure,根據pure得到usePureOnlyMemo,如果是pure模式那么actualChildProps的計算就會被memo,如果依賴沒變則不會重新計算。
如果是pure模式的話Connect組件則等于React.memo(ConnectFunction),這樣會阻止掉父組件更新導致的子組件不必要的更新,作用和PureComponent一樣,會進行一次屬性的淺對比,相等則不更新當前組件。
接下來看store更新是如何避免不必要的更新的。
function subscribeUpdates () {...const checkForUpdates = () => {...newChildProps = childPropsSelector(latestStoreState,lastWrapperProps.current)if (newChildProps === lastChildProps.current) {...} else {...forceComponentUpdateDispatch({type: 'STORE_UPDATED',payload: {error}})...}}... }上面的代碼可以看出來如果store更新新計算出來的newChildProps和lastChildProps.current(上一次更新的childProps)一樣的話就不會主動觸發當前組件更新(調用方法forceComponentUpdateDispatch)。
所以現在組要看newChildProps的計算過程,也就是childPropsSelector(latestStoreState, lastWrapperProps.current)的調用,它是怎么在多次調用的時候返回的引用是相同的(對象是相同的可以通過===嚴格等于)。
const selectorFactoryOptions = {...connectOptions,getDisplayName,methodName,renderCountProp,shouldHandleStateChanges,storeKey,displayName,wrappedComponentName,WrappedComponent }function createChildSelector(store) {return selectorFactory(store.dispatch, selectorFactoryOptions) }const childPropsSelector = useMemo(() => {// The child props selector needs the store reference as an input.// Re-create this selector whenever the store changes.return createChildSelector(store) }, [store])這里面的東西有點多而且很巧妙,所以下一章繼續解讀。
- 閱讀react-redux源碼 - 零
- 閱讀react-redux源碼 - 一
- 閱讀react-redux源碼(二) - createConnect、match函數的實現
- 閱讀react-redux源碼(三) - mapStateToPropsFactories、mapDispatchToPropsFactories和mergePropsFactories
- 閱讀react-redux源碼(四) - connectAdvanced、wrapWithConnect、ConnectFunction和checkForUpdates
- 閱讀react-redux源碼(五) - connectAdvanced中store改變的事件轉發、ref的處理和pure模式的處理
總結
以上是生活随笔為你收集整理的阅读react-redux源码(五) - connectAdvanced中store改变的事件转发、ref的处理和pure模式的处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阅读react-redux源码(四) -
- 下一篇: 阅读react-redux源码(六) -