繼 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.js
和 index.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) 複製代碼
從上面咱們能夠看出 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
組件觸發更新的原理,那麼話很少說,咱們來看到 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> } 複製代碼
看到這裏其實已經明白 Provider
組件的整個邏輯,即依據 store
或 previousState
的變化觸發訂閱退訂的生命週期以及通知更新操做。那麼再和 connect
高階組件函數配合實現 React Redux 的功能。顯然,到了這裏我想你們都應該能猜到, Provider
組件用於注入 Redux 並控制邏輯,connect
高階組件函數來處理用戶想要的 store
以 props
的形式傳入。咱們先來看到 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
爲例子。
即對入參進行以下順序校驗,優先知足即返回:
whenMapDispatchToPropsIsFunction
傳入識別爲函數whenMapDispatchToPropsIsMissing
未傳入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 源碼學習到此,感謝閱讀至此!