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

跨组件通信和数据共享不是一件容易的事,如果通过 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.mfbz.cn/a/252655.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux---压缩和解压缩命令

1. 压缩格式的介绍 Linux默认支持的压缩格式: .gz.bz2.zip 说明: .gz和.bz2的压缩包需要使用tar命令来压缩和解压缩.zip的压缩包需要使用zip命令来压缩&#xff0c;使用unzip命令来解压缩 压缩目的: 节省磁盘空间 2. tar命令及选项的使用 命令说明tar压缩和解压缩命令 …

网络 / day02 作业

1. TCP和UDP通信模型 1.1 TCP server #include <myhead.h>#define PORT 9999 #define IP "192.168.250.100"int main(int argc, const char *argv[]) {//1. create socketint sfd -1;if( (sfd socket(AF_INET, SOCK_STREAM, 0 ))-1 ){perror("socke…

CCF-CSP真题《202312-1 仓库规划》思路+python,c++,java满分题解

想查看其他题的真题及题解的同学可以前往查看&#xff1a;CCF-CSP真题附题解大全 试题编号&#xff1a;202312-1试题名称&#xff1a;仓库规划时间限制&#xff1a;1.0s内存限制&#xff1a;512.0MB问题描述&#xff1a; 问题描述 西西艾弗岛上共有 n 个仓库&#xff0c;依次编…

模块一——双指针:LCR 179.查找总价格为目标值的两个商品

文章目录 题目描述算法原理解法一&#xff1a;暴力解法(会超时&#xff09;解法二&#xff1a;对撞指针 代码实现解法一&#xff1a;暴力解法(超时&#xff09;解法二&#xff1a;对撞指针(时间复杂度为O(N)&#xff0c;空间复杂度为O(1)) 题目描述 题目链接&#xff1a;LCR 1…

C语言-Makefile

Makefile 什么是make&#xff1f; make 是个命令&#xff0c;是个可执行程序&#xff0c;用来解析 Makefile 文件的命令这个命令存放在 /usr/bin/ 什么是 makefile? makefile 是个文件&#xff0c;这个文件中描述了我们程序的编译规则咱们执行 make 命令的时候&#xff0c; m…

【Linux】在vim中批量注释与批量取消注释

在vim编辑器中&#xff0c;批量注释和取消注释的操作可以通过进入V-BLOCK模式、选择要注释或取消注释的内容、输入注释符号或选中已有的注释符号和按键完成。这些操作可以大大提高代码或文本的编写和修改效率&#xff0c;是vim编辑器中常用的操作之一。 1.在vim中批量注释的步…

Ubuntu22.04添加用户

一、查看已存在的用户 cat /etc/passwd 二、添加用户 sudo adduser xxx 除了密码是必须的&#xff0c;其他的都可以不填&#xff0c;直接回车即可 三、查看添加的用户 cat /etc/passwd 四、将新用户添加到sudo组 sudo adduser xxx sudo 五、删除用户 sudo delus…

线上业务优化之案例实战

本文是我从业多年开发生涯中针对线上业务的处理经验总结而来&#xff0c;这些业务或多或少相信大家都遇到过&#xff0c;因此在这里分享给大家&#xff0c;大家也可以看看是不是遇到过类似场景。本文大纲如下&#xff0c; 后台上传文件 线上后台项目有一个消息推送的功能&#…

Windows11安装python模块transformers报错Long Path处理

Windows11安装python模块transformers报错&#xff0c;报错信息如下 ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory: C:\\Users\\27467\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\\Local…

四. 基于环视Camera的BEV感知算法-BEVDet

目录 前言0. 简述1. 算法动机&开创性思路2. 主体结构3. 损失函数4. 性能对比总结下载链接参考 前言 自动驾驶之心推出的《国内首个BVE感知全栈系列学习教程》&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考 本次课程我们来学习下课程第四章——基于环视Cam…

Spring IoCDI

文章目录 一、Spring、Spring boot、Spring MVC之间的区别1. Spring 是什么2. 区别概述 一、Spring、Spring boot、Spring MVC之间的区别 1. Spring 是什么 Spring 是包含了众多工具方法的 IoC 容器 &#xff08;1&#xff09;容器 容器是用来容纳某种物品的基本装置&#xf…

DevEco Studio无法识别本地模拟器设备的解决方法

遇到了一个问题&#xff0c;之前测试无误的本地模拟器&#xff0c;运行后设备栏中无法识别了。 此时保持模拟器处于开启状态&#xff0c;关闭DevEco Studio窗口重新启动后&#xff0c;发现重新识别设备了。

vue项目中 CDN 是vue本身的依赖可以按需加载还是项目中所有的第三方库都可以按需加载?

这是我看到CDN简介时产生的问题 相信很多小伙伴会有 和我一样的疑问 在这里 我也统一回答一下 CDN&#xff08;内容分发网络&#xff09;是一种通过将数据分发到全球各个节点&#xff0c;以提供快速、可靠的内容传输的技术。在Vue项目中&#xff0c;CDN可以用于按需加载Vue本…

c语言中的static静态(1)static修饰局部变量

#include<stdio.h> void test() {static int i 1;i;printf("%d ", i); } int main() {int j 0;while (j < 5){test();j j 1;}return 0; } 在上面的代码中&#xff0c;static修饰局部变量。 当用static定义一个局部变量后&#xff0c;这时局部变量就是…

【TB作品】51单片机,具有报时报温功能的电子钟

2.具有报时报温功能的电子钟 一、功能要求: 1.显示室温。 2.具有实时时间显示。 3.具有实时年月日显示和校对功能。 4.具有整点语音播报时间和温度功能。 5.定闹功能,闹钟音乐可选。 6.操作简单、界面友好。 二、设计建议: 1.单片机自选(C51、STM32或其他单片机)。 2.时钟日历芯…

L1-047:装睡

题目描述 你永远叫不醒一个装睡的人 —— 但是通过分析一个人的呼吸频率和脉搏&#xff0c;你可以发现谁在装睡&#xff01;医生告诉我们&#xff0c;正常人睡眠时的呼吸频率是每分钟15-20次&#xff0c;脉搏是每分钟50-70次。下面给定一系列人的呼吸频率与脉搏&#xff0c;请你…

智能优化算法应用:基于松鼠算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于松鼠算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于松鼠算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.松鼠算法4.实验参数设定5.算法结果6.参考文献7.MA…

qt实现基本文件操作

先通过ui界面实现基本框架 接下来就要实现每个按键的功能了 我们先来实现新建的的功能&#xff0c;我们右键新建键&#xff0c;可以发现没有转到槽的功能&#xff0c;因此我们要自己写connect来建立关系。 private slots:void newActionSlot(); 在.h文件中加上槽函数。 conne…

PMP项目管理 - 风险管理

系列文章目录 PMP项目管理 - 质量管理 PMP项目管理 - 采购管理 PMP项目管理 - 资源管理 PMP项目管理 - 风险管理 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everything is for the future of dream weaving wings, let the dream fly in…

常见Appium相关问题及解决方案

问题1&#xff1a;adb检测不到设备 解决&#xff1a; 1.检查手机驱动是否安装&#xff08;win10系统不需要&#xff09;&#xff0c;去官网下载手机驱动或者电脑下载手机助手来辅助安装手机驱动&#xff0c;安装完成后卸载手机助手&#xff08;防止接入手机时抢adb端口造成干…
最新文章