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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 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,而且都很好理解,这源码没必要还是不要看了,太绕了一点都不享受 🤦‍♂️。

相关文章
|
28天前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
60 4
|
30天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
2天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
8天前
|
存储 人工智能 自然语言处理
高效档案管理案例介绍:文档内容批量结构化解决方案解析
档案文件内容丰富多样,传统人工管理耗时低效。思通数科AI平台通过自动布局分析、段落与标题检测、表格结构识别、嵌套内容还原及元数据生成等功能,实现档案的高精度分块处理和结构化存储,大幅提升管理和检索效率。某历史档案馆通过该平台完成了500万页档案的数字化,信息检索效率提升60%。
|
14天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
35 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
104 5
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
66 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0

推荐镜像

更多