【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.hqwc.cn/news/282201.html

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

相关文章

C语言-Makefile

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

C++软件调试与异常排查技术从入门到精通学习路线分享

目录 1、概述 2、全面了解引发C软件异常的常见原因 3、熟练掌握排查C软件异常的常见手段与方法 3.1、IDE调试 3.2、添加打印日志 3.3、分块注释代码 3.4、数据断点 3.5、历史版本比对法 3.6、Windbg静态分析与动态调试 3.7、使用IDA查看汇编代码 3.8、使用常用工具分…

连锁管理系统是什么?有哪些功能?

连锁管理系统帮助门店实现POS收银管理、门店管理、采购订货管理、线上商城搭建、供应链管理一体化管理系统&#xff0c;快速提高门店管理效率&#xff0c;无论你的门店有多少&#xff0c;连锁总部都能通过系统随时洞察监管门店的所有运营数据。 连锁管理系统由&#xff1a;1个…

【线性代数】期末速通!

1. 行列式的性质 1.1 求一个行列式的值 特殊地&#xff0c;对角线左下全为0&#xff0c;结果为对角线乘积。行 r 列 c 1.2 性质 某行&#xff08;列&#xff09;加上或减去另一行&#xff08;列&#xff09;的几倍&#xff0c;行列式不变某行&#xff08;列&#xff09;乘 …

使用Jemeter对HTTP接口压测

我们不应该仅仅局限于某一种工具&#xff0c;性能测试能使用的工具非常多&#xff0c;选择适合的就是最好的。笔者已经使用Loadrunner进行多年的项目性能测试实战经验&#xff0c;也算略有小成&#xff0c;任何性能测试&#xff08;如压力测试、负载测试、疲劳强度测试等&#…

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

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

【FPGA】Verilog:编码器 | 实现 4 到 2 编码器

0x00 编码器&#xff08;Encoder&#xff09; 编码器与解码器相反。当多台设备向计算机提供输入时&#xff0c;编码器会为每一个输入生成一个与设备相对应的信号&#xff0c;因此有多少比特就有多少输出&#xff0c;以数字形式表示输入的数量。 例如&#xff0c;如果有四个输…

基于junit4搭建自定义的接口自动化测试框架

随着业务的逐步稳定&#xff0c;对于接口的改动也会逐渐变少。更多的是对业务逻辑的优化&#xff0c;功能实现的完善。对于测试来说&#xff0c;重复繁琐的功能测试不仅效率低下&#xff0c;而且耗费一定的人力资源。笔者支持的信息流业务下的一个图文管理平台就是一个功能较为…

Java解决不同路径问题

Java解决不同路径问题 01 题目 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少…

秋招上岸记录咕咕咕了。

思考了一下&#xff0c;感觉并没有单独写这样一篇博客的必要。 能够写出来的&#xff0c;一些可能会对人有帮助的东西都做进了视频里面&#xff0c;未来会在blbl发布&#xff0c;目前剪辑正在施工中&#xff08;&#xff1f;&#xff09; 另外就是&#xff0c;那个视频里面使…

「Leetcode」滑动窗口—无重复字符的最长子串

&#x1f4bb;文章目录 &#x1f4c4;题目✏️题目解析 & 思路&#x1f4d3;总结 &#x1f4c4;题目 3. 无重复字符的最长子串 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s “abcabcbb” 输出: 3 解释: 因为无重复字…

小程序开发使用vant库

初始化项目步骤就不做阐述。 第一步&#xff1a;安装依赖 vant/weapp npm下载命令&#xff1a;npm i vant/weapp -S --production npm下载命令&#xff1a;yarn add vant/weapp -S --production 第二步 &#xff1a;修改配置 1、找到miniprogram文件下的app.json 将 app.j…