【react.js + hooks】基于事件机制的跨组件数据共享

news2025/1/18 4:38:37

跨组件通信和数据共享不是一件容易的事,如果通过 prop 一层层传递,太繁琐,而且仅适用于从上到下的数据传递;建立一个全局的状态 Store,每个数据可能两三个组件间需要使用,其他地方用不着,挂那么大个状态树也浪费了。当然了,有一些支持局部 store 的状态管理库,比如 zustand,我们可以直接使用它来跨组件共享数据。不过本文将基于事件机制的原理带来一个新的协同方案。

目标

vue3 中有 provide 和 inject 这两个 api,可以将一个组件内的状态透传到另外的组件中。那我们最终要实现的 hook 就叫 useProvide 和 useInject 吧。要通过事件机制来实现这两个 hook,那少不了具备事件机制的 hook,所以我们要先来实现一个事件发射器(useEmitter)和一个事件接收器(useReceiver)

事件 Hook 思路

  • 需要一个事件总线
  • 需要一对多的事件和侦听器映射关系
  • 需要具备订阅和取消功能
  • 支持命名空间来提供一定的隔离性
useEmitter

很简单,我们创建一个全局的 Map 对象来充当事件总线,在里面根据事件名和侦听器名存储映射关系即可。

代码不做太多解释,逻辑很简单,根据既定的命名规则来编排事件,注意重名的处理即可。

(Ukey 是一个生成唯一id的工具函数,你可以自己写一个,或者用nanoid等更专业的库替代)

import { useEffect, useContext, createContext } from "react";
import Ukey from "./utils/Ukey";

interface EventListener {
  namespace?: string;
  eventName: string;
  listenerName: string;
  listener: (...args: any[]) => void;
}

// 创建一个全局的事件监听器列表
const globalListeners = new Map<string, EventListener>();

// 创建一个 Context 来共享 globalListeners
const GlobalListenersContext = createContext(globalListeners);

export const useGlobalListeners = () => useContext(GlobalListenersContext);

interface EventEmitterConfig {
  name?: string;
  initialEventName?: string;
  initialListener?: (...args: any[]) => void;
  namespace?: string;
}

interface EventEmitter {
  name: string;
  emit: (eventName: string, ...args: any[]) => void;
  subscribe: (eventName: string, listener: (...args: any[]) => void) => void;
  unsubscribe: (eventName: string) => void;
  unsubscribeAll: () => void;
}

function useEmitter(
  name: string,
  config?: Partial<EventEmitterConfig>
): EventEmitter;
function useEmitter(config: Partial<EventEmitterConfig>): EventEmitter;
function useEmitter<M = {}>(
  name?: string,
  initialEventName?: string,
  // @ts-ignore
  initialListener?: (...args: M[typeof initialEventName][]) => void,
  config?: Partial<EventEmitterConfig>
): EventEmitter;

// @ts-ignore
function useEmitter<M = {}>(
  nameOrConfig?: string | Partial<EventEmitterConfig>,
  initialEventNameOrConfig?: string | Partial<EventEmitterConfig>,
  // @ts-ignore
  initialListener?: (...args: M[typeof initialEventNameOrConfig][]) => void,
  config?: Partial<EventEmitterConfig>
) {
  const globalListeners = useContext(GlobalListenersContext);

  // 根据参数类型确定实际的参数值
  let configActual: Partial<EventEmitterConfig> = {};

  if (typeof nameOrConfig === "string") {
    configActual.name = nameOrConfig;
    if (typeof initialEventNameOrConfig === "string") {
      configActual.initialEventName = initialEventNameOrConfig;
      configActual.initialListener = initialListener;
    } else if (typeof initialEventNameOrConfig === "object") {
      Object.entries(initialEventNameOrConfig).map(([key, value]) => {
        if (value !== void 0) {
          // @ts-ignore
          configActual[key] = value;
        }
      });
    }
  } else {
    configActual = nameOrConfig || {};
  }

  if (!configActual.name) {
    configActual.name = `_emitter_${Ukey()}`;
  }
  if (!configActual.namespace) {
    configActual.namespace = "default";
  }

  // 如果没有传入 name,使用 Ukey 方法生成一个唯一的名称
  const listenerName = configActual.name;

  const emit = (eventName: string, ...args: any[]) => {
    globalListeners.forEach((value, key) => {
      if (key.startsWith(`${configActual.namespace}_${eventName}_`)) {
        value.listener(...args);
      }
    });
  };

  const subscribe = (eventName: string, listener: (...args: any[]) => void) => {
    const key = `${configActual.namespace}_${eventName}_${listenerName}`;
    if (globalListeners.has(key)) {
      throw new Error(
        `useEmitter: Listener ${listenerName} has already registered for event ${eventName}`
      );
    }
    globalListeners.set(key, { eventName, listenerName, listener });
  };

  const unsubscribe = (eventName: string) => {
    const key = `${configActual.namespace}_${eventName}_${listenerName}`;
    globalListeners.delete(key);
  };

  const unsubscribeAll = () => {
    const keysToDelete: string[] = [];
    globalListeners.forEach((value, key) => {
      if (key.endsWith(`_${listenerName}`)) {
        keysToDelete.push(key);
      }
    });
    keysToDelete.forEach((key) => {
      globalListeners.delete(key);
    });
  };

  useEffect(() => {
    if (configActual.initialEventName && configActual.initialListener) {
      subscribe(configActual.initialEventName, configActual.initialListener);
    }
    return () => {
      globalListeners.forEach((value, key) => {
        if (key.endsWith(`_${listenerName}`)) {
          globalListeners.delete(key);
        }
      });
    };
  }, [configActual.initialEventName, configActual.initialListener]);

  return { name: listenerName, emit, subscribe, unsubscribe, unsubscribeAll };
}

export default useEmitter;
export { GlobalListenersContext };
useReceiver

我们在 useEmitter 的基础上封装一个 hook 来实时存储事件的值

import { useState, useEffect, useCallback } from "react";
import useEmitter from "./useEmitter";
import Ukey from "./utils/Ukey";
import { Prettify } from "./typings";

type EventReceiver = {
  stop: () => void;
  start: () => void;
  reset: (args: any[]) => void;
  isListening: boolean;
  // emit: (event: string, ...args: any[]) => void;
};

type EventReceiverOptions = {
  name?: string;
  namespace?: "default" | (string & {});
  eventName: string;
  callback?: EventCallback;
};

type EventCallback = (...args: any[]) => void;

function useReceiver(
  eventName: string,
  callback?: EventCallback
): [any[] | null, EventReceiver];
function useReceiver(
  options: Prettify<EventReceiverOptions>
): [any[] | null, EventReceiver];

function useReceiver(
  eventNameOrOptions: string | Prettify<EventReceiverOptions>,
  callback?: EventCallback
): [any[] | null, EventReceiver] {
  let eventName: string;
  let name: string;
  let namespace: string;
  let cb: EventCallback | undefined;

  if (typeof eventNameOrOptions === "string") {
    eventName = eventNameOrOptions;
    name = `_receiver_${Ukey()}`;
    namespace = "default";
    cb = callback;
  } else {
    eventName = eventNameOrOptions.eventName;
    name = eventNameOrOptions.name || `_receiver_${Ukey()}`;
    namespace = eventNameOrOptions.namespace || "default";
    cb = eventNameOrOptions.callback;
    if (cb) {
      if (callback) {
        console.warn(
          "useReceiver: Callback is ignored when options.callback is set"
        );
      } else {
        cb = callback;
      }
    }
  }

  const { subscribe, unsubscribe, emit } = useEmitter({
    name: name,
    namespace: namespace,
  });
  const [isListening, setIsListening] = useState(true);
  const [eventResult, setEventResult] = useState<any[] | null>(null);

  const eventListener = useCallback((...args: any[]) => {
    setEventResult(args);
    cb?.(...args);
  }, []);

  useEffect(() => {
    subscribe(eventName, eventListener);
    return () => {
      unsubscribe(eventName);
    };
  }, [eventName, eventListener]);

  const stopListening = useCallback(() => {
    unsubscribe(eventName);
    setIsListening(false);
  }, [eventName]);

  const startListening = useCallback(() => {
    subscribe(eventName, eventListener);
    setIsListening(true);
  }, [eventName, eventListener]);

  const reveiver = {
    stop: stopListening,
    start: startListening,
    reset: setEventResult,
    isListening,
    get emit() {
      return emit;
    },
  } as EventReceiver;

  return [eventResult, reveiver];
}

export default useReceiver;

这里我们开放了 emit,但在类型声明上隐藏它,因为使用者不需要它,留着 emit 是因为我们在接来下实现 useInject 还需要它。

共享 Hook 思路

有了 useEmitter 和 useReceiver 这两大基石后,一切都豁然开朗。我们只需要在 useEmitter 的基础上封装 useProvide,传入唯一键名,state 值和 setState,将其和事件绑定即可,注意这里额外订阅了一个 query 事件,来允许其监听者主动请求提供者广播一次数据(用处后面提)。

useProvide
import { Dispatch, SetStateAction, useEffect } from "react";
import useEmitter from "./useEmitter";

export function useProvide<T = any>(
  name: string,
  state: T,
  setState?: Dispatch<SetStateAction<T>>
) {
  const emitter = useEmitter(`__Provider::${name}`, {
    namespace: "__provide_inject__",
    initialEventName: `__Inject::${name}::query`,
    initialListener() {
      emitter.emit(`__Provider::${name}`, state, setState);
    },
  });
  useEffect(() => {
    emitter.emit(`__Provider::${name}`, state, setState);
  }, [name, state, setState]);
}

export default useProvide;
useInject

useInject 只需要封装 useReceiver 并返回 state即可,注意在 useInject 挂载之初,我们需要主动向提供者请求一次同步,因为提供者通常情况下比注入者挂载的更早,提供者初始主动同步的那一次,绝大多数注入者并不能接收到。

import { Dispatch, SetStateAction, useEffect } from "react";
import useReceiver from "./useReceiver";
import UKey from "./utils/Ukey";

/**
 * useInject is a hook that can be used to inject a value from a provider.
 * 
 * ---
 * ### Parameters
 * - `name` - The name of the provider to inject from.
 * 
 * ---
 * ### Returns
 * - [0]`value` - The value of the provider.
 * - [1]`setValue` - A function to set the value of the provider.
 */
function useInject<
  T extends Object = { [x: string]: any },
  // @ts-ignore
  K extends string = keyof T,
  // @ts-ignore
  V = K extends string ? T[K] | undefined : any
  // @ts-ignore
>(name: K): [V, Dispatch<SetStateAction<V>>] {
  // @ts-ignore
  const [result, { emit }] = useReceiver({
    name: `__Inject::${name}_${UKey()}`,
    eventName: `__Provider::${name}`,
    namespace: "__provide_inject__",
  });

  const query = () => emit(`__Inject::${name}::query`, true);

  useEffect(() => {
    query();
  }, []);

  return [result?.[0], result?.[1]];
}

export default useInject;

然后你就可以像这样快乐的共享数据了:

import useInject from "@/hooks/useInject";
import useProvide from "@/hooks/useProvide";
import { Button } from "@mui/material";
import { useState } from "react";

type Person = {
  name: string;
  age: number;
};

const UseProvideExample = () => {
  const [state, setState] = useState<Person>({
    name: "Evan",
    age: 20,
  });
  useProvide("someone", state);
  return (
    <>
      <Button
        onClick={() =>
          setState({ ...state, name: state.name === "Evan" ? "Nave" : "Evan" })
        }
      >
        {state.name}
      </Button>
      <Button onClick={() => setState({ ...state, age: state.age + 1 })}>
        {state.age}
      </Button>
    </>
  );
};

const UseInjectExample = () => {
  const [state] = useInject<{ someone: Person }>("someone");
  const [state2] = useInject<{ someone: Person }>("someone");
  return (
    <>
      <div style={{ display: "flex" }}>
        <span>{state?.name}</span>
        <div style={{ width: "2rem" }}></div>
        <span>{state?.age}</span>
      </div>
      <div style={{ display: "flex" }}>
        <span>{state2?.name}</span>
        <div style={{ width: "2rem" }}></div>
        <span>{state2?.age}</span>
      </div>
    </>
  );
};

const View = () => {
  return (
    <>
      <h4>UseProvide</h4>
      <UseProvideExample />
      <h4>Inject</h4>
      <UseInjectExample />
    </>
  );
};

Demo 效果图:
useInject 效果图
Bingo! 用于跨组件协同的 useProvide 和 useInject 就这样实现了!
(PS : 我这里的 useProvide 和 useInject 并没有开发命名空间,你们可以拓展参数来提供更细粒度的数据隔离)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1318170.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件设计师——计算机组成原理(三)

&#x1f4d1;前言 本文主要是【计算机组成原理】——软件设计师——计算机组成原理的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 …

解决nuxt3报错:The requested module xxx does not provide an export named ‘PropType‘

现象如下&#xff1a; 从表象上就是typescript找不到PropType的类型声明 原因&#xff1a;这是vue3已知的type类型导入时存在的一个问题&#xff0c;而且一直没有得到解决 No matching export for import typescript interface Issue #2117 vitejs/vite GitHub 代码里面导…

人工智能强化学习:智能体自我进化的探索与挑战

导言 人工智能强化学习作为一种模仿人类学习方式的技术&#xff0c;近年来在机器学习领域取得了令人瞩目的进展。强化学习作为一种通过智能体与环境的交互学习的技术&#xff0c;与其他模型的融合不仅可以提升学习效果&#xff0c;还能在更广泛的领域中实现智能体的决策。本文将…

python学习,1.变量和简单的数据类型

一、编写文章的目的 1.这是为了初学者而写的&#xff0c;学习python比较简单然后上手&#xff0c;也会过滤一些&#xff0c;如果没有提起到的&#xff0c;可以在学习的时候进行补充 2.相对来说&#xff0c;上手难度不会很难。 二、内容 1.让首字母大写&#xff1b;字母都大写…

Linux查看进程PID以及杀掉进程的方法

目录 参考链接 前言 查看进程PID PS命令 ps -le命令 查找父进程 杀死进程 参考链接 【Linux 】 ps命令详解&#xff0c;查看进程pid_linux查看pid 对应的程序-CSDN博客 Linux查看进程PID的方法&#xff08;linux查进程的pid&#xff09;附带自动kill 掉_linux查看pid 对…

大数据分析与应用实验任务十二

大数据分析与应用实验任务十二 实验目的&#xff1a; 通过实验掌握spark机器学习库本地向量、本地矩阵的创建方法&#xff1b; 熟悉spark机器学习库特征提取、转换、选择方法&#xff1b; 实验任务&#xff1a; 一、逐行理解并参考编写运行教材8.3.1、8.3.3节各个例程代码…

linux脚本中 #!/bin/sh、#!/bin/bash

我们通常看到的脚本文件总是有以下这样的开头&#xff1a; #!/bin/bash本文解释一下这是什么&#xff0c;以及为什么要写它。 首先解释一下 #! &#xff0c;因为 #!有个专有的名词&#xff0c;叫 shebang 发音类似中文的 “蛇棒” 。为什么叫 shebang 呢&#xff1f; 首先 #…

【面向对象】C++/python/java的多态比较

一、面向对象的主要特点 封装&#xff1a;封装是把数据和操作数据的方法绑定在一起&#xff0c;对数据的访问只能通过已定义的接口。这可以保护数据不被外部程序直接访问或修改&#xff0c;增强数据的安全性。继承&#xff1a;继承是一种联结类的层次模型&#xff0c;并且允许…

1850_emacs_org-download在Windows上的使用

Grey 全部学习内容汇总&#xff1a; https://github.com/greyzhang/g_org 1850_emacs_org-download在Windows上的使用 对我来说&#xff0c;使用emacs很大的一个挑战是在Windows上&#xff0c;emacs的配置会比Linux上麻烦一些。而且&#xff0c;通常来说Windows上的体验会差…

SQL进阶理论篇(九):为什么不存在完美的索引

文章目录 简介索引片和过滤因子如何通过宽表避免回表什么是过滤因子理想索引设计&#xff1a;三星索引为什么很难存在理想的索引设计&#xff1f;参考文献 简介 本节将主要介绍以下部分&#xff1a; 什么是索引片&#xff0c;什么是过滤因子&#xff1f;设计索引的时候&#…

Python装饰器新境界:详解装饰器重载内置操作

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Python装饰器新境界&#xff1a;详解装饰器重载内置操作&#xff0c;全文3900字&#xff0c;阅读大约15分钟。 Python装饰器重载内置操作&#xff0c;我们通常指的是使用装饰…

Sqoop安装与配置-shell脚本一键安装配置

文章目录 前言一、使用shell脚本一键安装1. 复制脚本2. 增加执行权限3. 执行脚本4. 加载用户环境变量5. 查看是否安装成功 总结 前言 本文介绍了如何使用Shell脚本一键安装Sqoop。Sqoop是一个用于在Apache Hadoop和结构化数据存储&#xff08;如关系数据库&#xff09;之间传输…

Source Insight使用

之前一直使用VS code阅读kernel源码&#xff0c;有时候函数跳转有些问题。最近换成了Source Insight软件&#xff0c;发现真不错。就是需要一些学习成本&#xff0c;简单记录一下如何使用吧。 1、下载安装&#xff1a; 首先肯定是要下载安装&#xff0c;这个就不写了&#xf…

FFmpeg——在Vue项目中使用FFmpeg(安装、配置、使用、SharedArrayBuffer、跨域隔离、避坑...)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

滑动窗口训练

1.原理 我们用这道题目 LCR 008. 长度最小的子数组 来讲解“滑动窗口”的解法。 1.1.暴力解法 遍历每一个子数组&#xff08;都要大于等于 7&#xff09;&#xff0c;最统计出最小的数组。 这样做的话&#xff0c;划分左右区间&#xff08;left 和 right&#xff09;就需要…

怎样长时间保持SSH会话连接不断开?

操作场景 使用SSH方式登录CentOS Stream操作系统的云服务器时&#xff0c;过一段时间就会自动断开连接。 该文档适用于CentOS/EulerOS系统。 操作方法 编辑/etc/ssh/sshd_config文件设置心跳&#xff0c;保持连接。 编辑/etc/ssh/sshd_config&#xff0c;添加配置项&#x…

Flink系列之:监控反压

Flink系列之&#xff1a;监控反压 一、反压二、Task 性能指标三、示例四、反压状态 Flink Web 界面提供了一个选项卡来监控正在运行 jobs 的反压行为。 一、反压 如果你看到一个 task 发生 反压警告&#xff08;例如&#xff1a; High&#xff09;&#xff0c;意味着它生产数…

Android动画

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览三、动画实现3.1 帧动画资源文件中实现…

Linux线程的设计

文章目录 一.理解Linux线程的本质进程地址空间是进程访问系统资源的窗口Linux系统中,线程是比进程更轻量级的执行流 二.Linux线程独立运行的原理三.基础线程控制 一.理解Linux线程的本质 进程地址空间是进程访问系统资源的窗口 Linux系统中,线程是比进程更轻量级的执行流 线程…

IIS如何本地部署网站,作为局域网内的服务器

文章目录 IIS本地部署WebService1.使用IIS及WebService的原因:2.相关文件说明及网络条件说明&#xff1a;&#xff08;1&#xff09;文件说明&#xff1a;&#xff08;2&#xff09;网络条件说明&#xff1a; 3.IIS安装与配置&#xff1a;第一步&#xff1a;安装第二步&#xf…