【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-ignoreinitialListener?: (...args: M[typeof initialEventName][]) => void,config?: Partial<EventEmitterConfig>
): EventEmitter;// @ts-ignore
function useEmitter<M = {}>(nameOrConfig?: string | Partial<EventEmitterConfig>,initialEventNameOrConfig?: string | Partial<EventEmitterConfig>,// @ts-ignoreinitialListener?: (...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-ignoreconfigActual[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-ignoreK extends string = keyof T,// @ts-ignoreV = K extends string ? T[K] | undefined : any// @ts-ignore
>(name: K): [V, Dispatch<SetStateAction<V>>] {// @ts-ignoreconst [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 (<><ButtononClick={() =>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.mzph.cn/news/229068.shtml

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

相关文章

MySQL日志管理,备份与恢复

备份的主要目的是灾难恢复&#xff0c;备份还可以测试应用、回滚数据修改、查询历史数据、审计等。 而备份、恢复中&#xff0c;日志起到了很重要的作用 MySQL日志管理是数据库管理中的一个重要方面&#xff0c;它可以用于诊断问题、监控性能、进行故障恢复等。MySQL主要有几种…

【合成数字】合成类游戏-uniapp项目开发流程详解

以前玩过2048游戏&#xff0c;从中发现规律&#xff0c;想到跟合成类游戏相似&#xff0c;知道为什么很相似吗&#xff0c;在这里&#xff0c;做一个数字合成游戏玩玩吧&#xff0c;感兴趣的话可以看看&#xff0c;这里给大家讲一讲数字合成游戏的开发过程。 文章目录 创建项目…

Pandas-DataFtame的索引与切片(第3讲)

Pandas-DataFtame的索引与切片(第3讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ�…

【动态读取配置文件】ParameterTool读取带环境的配置信息

不同环境Flink配置信息是不同的&#xff0c;为了区分不同环境的配置文件&#xff0c;使用ParameterTool工具读取带有环境的配置文件信息 区分环境的配置文件 三个配置文件&#xff1a; flink.properties&#xff1a;决定那个配置文件生效 flink-dev.properties&#xff1a;测…

主持知识竞赛类节目的一般流程是什么

竞争性的团队活动&#xff0c;更适合青年的特点&#xff0c;更容易得到青年的支持&#xff0c;也是"离教于乐"的好方式。 这类活动可从内容和特点上分为知识性竞赛&#xff0c;技能性竞赛&#xff0c;文娱性竞赛&#xff0c;体育竞技性竞赛等形式。 知识性竞赛&…

【VMware安装及虚拟机配置】

1. 下载VMware 进入 VMware Workstation 17 Pro下载链接 下拉到如下位置&#xff0c;点击DOWNLOAD 2. 安装VMware 参考&#xff1a;虚拟机VMware下载与安装教程 本次安装是vmware 17&#xff0c;安装步骤差不多&#xff0c;只参考第二部分即可。 3. 激活VMware 密钥&…

【Qt QML入门】TextInput

TextInput&#xff1a;单行文本输入框。 TextInput除了光标和文本外&#xff0c;默认没有边框等效果。 import QtQuick import QtQuick.Window import QtQuick.ControlsWindow {id: winwidth: 800height: 600visible: truetitle: qsTr("Hello World")//单行文本输…

HarmonyOS开发实战:如何实现一个运动排名榜页面

HarmonyOS开发实战&#xff1a;如何实现一个运动排名榜页面 代码仓库&#xff1a; 运动排名榜页面 项目介绍 本项目使用声明式语法和组件化基础知识&#xff0c;搭建一个可刷新的排行榜页面。在排行榜页面中&#xff0c;使用循环渲染控制语法来实现列表数据渲染&#xff0c;…

03 使用Vite开发Vue3项目

概述 要使用vite创建Vue3项目&#xff0c;有很多种方式&#xff0c;如果使用命令&#xff0c;则推荐如下命令&#xff1a; # 使用nvm将nodejs的版本切换到20 nvm use 20# 全局安装yarn npm install -g yarn# 使用yarnvite创建项目 yarn create vite不过&#xff0c;笔者更推荐…

LeedCode刷题---滑动窗口问题(二)

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、将X减到0的最小操作数 题目链接&#xff1a;将 x 减到 0 的最小操作数 题目描述 给你一个整数数组 nums 和一个整数 x 。每一…

mysql的负向条件查询会不会使用索引

mysql的负向条件查询&#xff0c;例如not in&#xff0c;会不会使用索引&#xff1f; 其实&#xff0c;mysql还是会尽量利用索引。如果查询的列上有索引&#xff0c;并且索引能够覆盖查询所需的列&#xff0c;那么mysql可能会使用索引来获取结果&#xff0c;而不是进行全表扫描…

2024中国国际大数据产业博览会年度主题征集公告

2024中国国际大数据产业博览会年度主题征集公告 中国国际大数据产业博览会&#xff08;以下简称数博会&#xff09;&#xff0c;是全球首个以大数据为主题的国家级博览会&#xff0c;由国家发展和改革委员会、工业和信息化部、国家互联网信息办公室和贵州省人民政府共同主办&am…

ADB命令安装卸载手机APP

前言 手机内置的浏览器很多广告&#xff0c;推荐的新闻也很多负面的新闻&#xff0c;所以就想卸载内置的手机app&#xff0c;不过现在很多手机都是限制了内置的软件都不能卸载&#xff0c;以前随便获取一下root权限&#xff0c;也是可以卸载的&#xff0c;不过最近搞了一下&am…

【POI的如何做大文件的写入】

&#x1f513;POI如何做大文件的写入 &#x1f3c6;文件和POI之间的区别是什么&#xff1f;&#x1f3c6;POI对于当今的社会发展有何重要性&#xff1f;&#x1f3c6;POI大文件的写入&#x1f396;️使用XSSF写入文件&#x1f396;️使用SXSSFWorkbook写入文件&#x1f396;️对…

设计可编辑表格组件

前言 什么是可编辑表格呢&#xff1f;简单来说就是在一个表格里面进行表单操作&#xff0c;执行增删改查。这在一些后台管理系统中是尤为常见的。 今天我们根据vue2 element-ui来设计一个表单表格组件。&#xff08;不涉及完整代码&#xff0c;想要使用完整功能可以看底部连…

ReenterLock重入锁

synchronized就是一种最简单的控制方法&#xff0c;它决定了一个线程释放可以访问临界区资源。 同时&#xff0c;Object.wait()方法和Object.notify()方法起到了线程等待和通知的作用。 ReenterLock重入锁可以完全替代关键字Synchoronized.重入锁是Synchoronized、Object.wait(…

[楚慧杯 2023] web

文章目录 eaaevalupload_shell eaaeval 打开题目&#xff0c;源码给了用户密码 登陆后啥也没有&#xff0c;扫一下发现源码泄露www.zip <?php class Flag{public $a;public $b;public function __construct(){$this->a admin;$this->b admin;}public function _…

C++ list常用操作

目录 一、介绍 二、list的常用操作 1、构造 2、迭代器 3、元素访问 4、容量操作 一、介绍 std::list文档链接 list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。list的底层是双向链表结构&#xff0c;双向链表中每个…

独立看门狗 IWDG

看门狗介绍 "看门狗"通常指的是计算机科学和信息技术领域中的一种技术或设备&#xff0c;用于监控系统的运行状态&#xff0c;并在系统出现故障或异常情况时采取相应的措施。这种技术或设备起到类似于守卫的作用&#xff0c;确保系统的稳定性和可靠性。 在计算机系统…

beebox靶场A1 low 命令注入通关教程(上)

一&#xff1a;html注入 get HTML注入&#xff0c;就是当用户进行输入时&#xff0c;服务器没有对用户输入的数据进行过滤或转义&#xff0c;导致所有输入均被返回前端&#xff0c;网页解析器会将这些数据当作html代码进行解析,这就导致一些恶意代码会被正常执行。 首先进行简…