看文档不如看源码? - React-Redux 源码解析

简介: react-redux 的源码不太适合阅读,本身常用的 API 不多,只是 Provider 和 connect,而且都很好理解,这源码没必要还是不要看了,太绕了一点都不享受 🤦‍♂️。

网络异常,图片无法展示
|

前排提示:

文件结构

老规矩,先看下文件结构(去掉一些不太重要的文件)。

├── 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 用到的默认的 ContextshallowEqual 就是一个浅比较函数。

首先我们看下 subscription,因为 Providerconnect 中都有使用。代码我就不贴了,贴下定义:

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 添加到 parentSubstore 的订阅中,然后调用 createListenerCollection 创建 listenerCollectiononStateChange 需要手动添加,后面可以看到。
  • listenerCollection中有以下几个方法
  • subscribe 添加订阅,这里的 listener 为链表结构
  • clear 清空所有的订阅
  • notify 调用 batch 触发所有的 listener
  • getlistener 转换为数组返回
  • tryUnsubscribe 会调用 clear
  • notifyNestedSubs 调用 notify
  • addNestedSub(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 外还接受 contextserverStatecontext 用于自定义 context

这里还用到 useIsomorphicLayoutEffect,该 hook 可以认为就是 useLayoutEffect,只是因为在 ssr 时使用 useLayoutEffect 会报错,所以在服务端替换为 useEffect。这里主要用来处理 subscription 的注册和取消。

export const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;
复制代码

可以看到这里 subscriptiononStateChange 就是 notifyNestedSubs,所以在被包裹的组件中调用 notifyNestedSubs 时会触发此处的此处 subscriptionnotifyNestedSubs,从而将发布向上传递。讲真这里的代码是真的绕,感觉可以重新梳理一遍,比如可以保存 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,还有传入的 storesubscriptionstore 就是 reduxcreateStore 创建的 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;
复制代码

看下几个参数:

  • mapStateToPropsstorestate 转换为 props
  • mapDispatchToPropsdispatch 转换为 props
  • mergeProps 将上述的 props 和原有的 props 进行合并
  • 下面几个 equal 函数默认是 strictEqual 用于比较各种 props,特殊情况可以做一些性能优化或精细比较
  • 可以看到这里还有 context 参数,由于 Provider 可以接受自定义 context,如果不在此处声明会导致 connect 中的 context 丢失。

返回值则是一个 hoc 方法:wrapWithConnectconnect 的主要逻辑也都在其中。

connect 上方的代码主要是处理 mapStateToPropsmapDispatchToPropsmergeProps 几个函数,并通过 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);
复制代码

此处的 WrappedComponenthoc 中传入的原始组件。首先是处理 displayName ,然后到 connect.ts:518 处的 ConnectFunction 便是 hoc 后返回的新组件了,紧接着就是 forwardRef的处理,displayName 的挂载和 hoistStatics 进行静态变量的提升。

这里说下静态变量的提升,主要是为了避免 connect 后组件的静态变量丢失的情况,如一些早期常用的一些 defaultPropspropTypes 等。

重点看下 ConnectFunction,此处代码较多,且比较复杂,就不贴了,我大概梳理下逻辑:

  1. 该组件中会创建自己的 subscription
  2. 该组件会包裹 Context 并将 subscription 覆盖向下传递
  3. 其中会判定 store 的来源做一些区分处理

简单说就是通过 connect 可以将顶部的 store 配合 mapStateToProps 等转换为 props,并且其中每个 connect 都会包裹 Context,并且将 subscription 覆盖向下传递,实现了类似事件的冒泡机制。

其它部分

其它还包括 useDispatchuseReduxContextuseStore 源码比较简单,useSelector 现挖个坑,后面有时间再补。

总结

从上面可以看出,react-redux 的源码不太适合阅读,本身常用的 API 不多,只是 Providerconnect,而且都很好理解,这源码没必要还是不要看了,太绕了一点都不享受 🤦‍♂️。

相关文章
|
7月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
654 29
|
7月前
|
人工智能 自然语言处理 搜索推荐
ViDoRAG:开源多模态文档检索框架,多智能体推理+图文理解精准解析文档
ViDoRAG 是阿里巴巴通义实验室联合中国科学技术大学和上海交通大学推出的视觉文档检索增强生成框架,基于多智能体协作和动态迭代推理,显著提升复杂视觉文档的检索和生成效率。
344 8
ViDoRAG:开源多模态文档检索框架,多智能体推理+图文理解精准解析文档
|
7月前
|
Web App开发 移动开发 前端开发
React音频播放器样式自定义全解析:从入门到避坑指南
在React中使用HTML5原生&lt;audio&gt;标签时,开发者常面临视觉一致性缺失、样式定制局限和交互体验割裂等问题。通过隐藏原生控件并构建自定义UI层,可以实现完全可控的播放器视觉风格,避免状态不同步等典型问题。结合事件监听、进度条拖拽、浏览器兼容性处理及性能优化技巧,可构建高性能、可维护的音频组件,满足跨平台需求。建议优先使用成熟音频库(如react-player),仅在深度定制需求时采用原生方案。
226 12
|
7月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
187 4
|
7月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
存储 前端开发 JavaScript
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
|
7月前
|
机器学习/深度学习 人工智能 文字识别
从“泛读”到“精读”:合合信息文档解析如何让大模型更懂复杂文档?
随着deepseek等大模型逐渐步入视野,理论上文档解析工作应能大幅简化。 然而,实际情况却不尽如人意。当前的多模态大模型虽然具备强大的视觉与语言交互能力,但在解析非结构化文档时,仍面临复杂版式、多元素混排以及严密逻辑推理等挑战。
195 0
|
7月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
5月前
|
缓存 前端开发 数据安全/隐私保护
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
如何使用组合组件和高阶组件实现复杂的 React 应用程序?
225 68
|
5月前
|
缓存 前端开发 Java
在 React 中,组合组件和高阶组件在性能方面有何区别?
在 React 中,组合组件和高阶组件在性能方面有何区别?
208 67

推荐镜像

更多
  • DNS