前排提示:
- 请先看上篇 Redux 源码全解析
- 此处源码版本为:720f0ba
文件结构
老规矩,先看下文件结构(去掉一些不太重要的文件)。
├── components │ ├── Context.ts │ ├── Provider.tsx │ └── connect.tsx ├── connect ├── exports.ts ├── hooks │ ├── useDispatch.ts │ ├── useReduxContext.ts │ ├── useSelector.ts │ └── useStore.ts ├── index.ts ├── next.ts ├── types.ts └── utils 复制代码
说实话看源码前我一直以为 react-redux 会比 redux 代码量更小,失算了。🤦♂️
源码分析
核心源码
不同于 redux,这里导出包裹两部分,一个是 index.ts 中导出的 batch(就是 react-dom/react-native 中的 unstable_batchedUpdates),一个是 exports.ts 中的导出。
export { Provider, ReactReduxContext, connect, useDispatch, useSelector, useStore, createDispatchHook, createSelectorHook, createStoreHook, shallowEqual }; 复制代码
其中 ReactReduxContext 就是 Provider 用到的默认的 Context,shallowEqual 就是一个浅比较函数。
首先我们看下 subscription,因为 Provider 和 connect 中都有使用。代码我就不贴了,贴下定义:
function createSubscription(store: any, parentSub?: Subscription): Subscription; const subscription: Subscription = { addNestedSub, notifyNestedSubs, handleChangeWrapper, isSubscribed, trySubscribe, tryUnsubscribe, getListeners: () => listenerCollection }; 复制代码
源码中 listenerCollection 也叫 listeners,为了避免混淆,下面称为 listenerCollection
parentSub可以认为是最顶层的subscription实例trySubscribe会调用addNestedSub订阅handleChangeWrapper,其实就是将subscription.onStateChange添加到parentSub或store的订阅中,然后调用createListenerCollection创建listenerCollection。onStateChange需要手动添加,后面可以看到。listenerCollection中有以下几个方法
subscribe添加订阅,这里的listener为链表结构clear清空所有的订阅notify调用batch触发所有的listenerget将listener转换为数组返回
tryUnsubscribe会调用clearnotifyNestedSubs调用notifyaddNestedSub(listener)调用trySubscribe并添加listener
说实话逻辑有点绕,我们再看下 Provider,正好看下 subscription 的使用
function Provider<A extends Action = AnyAction>({ store, context, children, serverState }: ProviderProps<A>) { const contextValue = useMemo(() => { const subscription = createSubscription(store); return { store, subscription, getServerState: serverState ? () => serverState : undefined }; }, [store, serverState]); const previousState = useMemo(() => store.getState(), [store]); useIsomorphicLayoutEffect(() => { const { subscription } = contextValue; subscription.onStateChange = subscription.notifyNestedSubs; subscription.trySubscribe(); if (previousState !== store.getState()) { subscription.notifyNestedSubs(); } return () => { subscription.tryUnsubscribe(); subscription.onStateChange = undefined; }; }, [contextValue, previousState]); const Context = context || ReactReduxContext; return <Context.Provider value={contextValue}>{children}</Context.Provider>; } 复制代码
可以看到 Provider 除了 store 外还接受 context 和 serverState,context 用于自定义 context。
这里还用到 useIsomorphicLayoutEffect,该 hook 可以认为就是 useLayoutEffect,只是因为在 ssr 时使用 useLayoutEffect 会报错,所以在服务端替换为 useEffect。这里主要用来处理 subscription 的注册和取消。
export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; 复制代码
可以看到这里 subscription 的 onStateChange 就是 notifyNestedSubs,所以在被包裹的组件中调用 notifyNestedSubs 时会触发此处的此处 subscription 的 notifyNestedSubs,从而将发布向上传递。讲真这里的代码是真的绕,感觉可以重新梳理一遍,比如可以保存 parentSub 然后调用其方法来将发布向上传递。
serverState 则是在 SSR 时客户端 hydration 时使用。这里结合文档看下场景:
const preloadedState = window.__PRELOADED_STATE__; const clientStore = configureStore({ reducer: rootReducer, preloadedState }); hydrateRoot( document.getElementById('root'), <Provider store={clientStore} serverState={preloadedState}> <App /> </Provider> ); 复制代码
服务端将初始化的 state 写入 window下,hydrate 时会将该 state 传入 serverState,从而避免注水后 state 不一致的问题。
再看下 contextValue,其中除了刚刚讲到的 getServerState,还有传入的 store 和 subscription。store 就是 redux 的 createStore 创建的 store。
再看下 connect 部分,居然有七八百行,离谱。先看下 ts 定义:
function connect<TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}, State = unknown>( mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, State>, mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>, mergeProps?: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>, { pure, areStatesEqual = strictEqual, areOwnPropsEqual = shallowEqual, areStatePropsEqual = shallowEqual, areMergedPropsEqual = shallowEqual, forwardRef = false, context = ReactReduxContext }: ConnectOptions<unknown, unknown, unknown, unknown> = {} ): unknown; 复制代码
看下几个参数:
mapStateToProps将store的state转换为propsmapDispatchToProps将dispatch转换为propsmergeProps将上述的props和原有的props进行合并- 下面几个
equal函数默认是strictEqual用于比较各种props,特殊情况可以做一些性能优化或精细比较 - 可以看到这里还有
context参数,由于Provider可以接受自定义context,如果不在此处声明会导致connect中的context丢失。
返回值则是一个 hoc 方法:wrapWithConnect,connect 的主要逻辑也都在其中。
connect 上方的代码主要是处理 mapStateToProps、mapDispatchToProps 和 mergeProps 几个函数,并通过 mapStateToProps 来确定 shouldHandleStateChanges,用于判定 state 变更时是否需要处理。
下面从 wrapWithConnect 提出几个重要部分代码看下具体逻辑:
const displayName = `Connect(${wrappedComponentName})`; const selectorFactoryOptions: SelectorFactoryOptions; function ConnectFunction<TOwnProps>(props: InternalConnectProps & TOwnProps); let Connect = React.memo(ConnectFunction); Connect.WrappedComponent = WrappedComponent; Connect.displayName = ConnectFunction.displayName = displayName; if (forwardRef) { Connect = React.forwardRef(function forwardConnectRef(props, ref) { return <Connect {...props} reactReduxForwardedRef={ref} />; }); } return hoistStatics(Connect, WrappedComponent); 复制代码
此处的 WrappedComponent 即 hoc 中传入的原始组件。首先是处理 displayName ,然后到 connect.ts:518 处的 ConnectFunction 便是 hoc 后返回的新组件了,紧接着就是 forwardRef的处理,displayName 的挂载和 hoistStatics 进行静态变量的提升。
这里说下静态变量的提升,主要是为了避免 connect 后组件的静态变量丢失的情况,如一些早期常用的一些 defaultProps、propTypes 等。
重点看下 ConnectFunction,此处代码较多,且比较复杂,就不贴了,我大概梳理下逻辑:
- 该组件中会创建自己的
subscription - 该组件会包裹
Context并将subscription覆盖向下传递 - 其中会判定
store的来源做一些区分处理
简单说就是通过 connect 可以将顶部的 store 配合 mapStateToProps 等转换为 props,并且其中每个 connect 都会包裹 Context,并且将 subscription 覆盖向下传递,实现了类似事件的冒泡机制。
其它部分
其它还包括 useDispatch、useReduxContext、useStore 源码比较简单,useSelector 现挖个坑,后面有时间再补。
总结
从上面可以看出,react-redux 的源码不太适合阅读,本身常用的 API 不多,只是 Provider 和 connect,而且都很好理解,这源码没必要还是不要看了,太绕了一点都不享受 🤦♂️。