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

news/2024/10/20 5:37:08/

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

相关文章

普冉(PUYA)单片机开发笔记 [完结篇]:使用体会

失败的移植&#xff1a;FreeRTOS 当使用了 PY32F003 的各种接口和功能后&#xff0c;手痒痒想把 FreeRTOS 也搬到这个 MCU 上&#xff0c;参考 STM32 和 GD32 对 FreeRTOS 的移植步骤&#xff0c;把 FreeRTOS v202212.00 版本的源码搬到了 Keil 工程中&#xff0c;编译倒是通过…

[计网00] 计算机网络开篇导论

目录 前言 计算机网络的概念 计算机网络的分层 计算机网络的分类 网络的标准化工作和相关组织 计算机网络的性能指标 前言 计算机网络在我们的日常生活中无处不在 在网络会有各种各样的协议和封装 保证我们的信息完整,无误的在各个客户端之前传输 计算机网络的概念 四…

06. Python模块

目录 1、前言 2、什么是模块 3、Python标准库模块 3.1、os模块 3.2、datetime 模块 3.3、random模块 4、自定义模块 4.1、创建和使用 4.2、模块命名空间 4.3、作用域 5、安装第三方依赖 5.1、使用 pip 安装单个依赖 5.2、从 requirements.txt 安装依赖 5.3、安装指…

城市智能图书柜需求说明书

1. 简介 1.1 项目概括 本项目主要实现智能图书柜对图书的借出、还回、续借、查询、上下架、盘点的功能&#xff0c;对于读者&#xff0c;可以进行读者证的办理&#xff0c;读者信息的录入和完善。 1.2 项目背景 ​ 目前大量读者距离图书馆较远&#xff0c;无法方便、快捷地…

maven构建单个模块,遇到错误:Could not find the selected project in the reactor

mvn install -pl modulename -am -pl 参数用于指定要构建的子模块&#xff0c;可以通过指定子模块的 artifactId 或者模块路径来指定。在命令中使用 -pl my-submodule 表示只构建名为 my-submodule 的子模块。 -am 参数用于自动构建依赖的模块。如果当前构建的模块依赖于其他…

Axure中动态面板使用及轮播图多种登录方式左侧导航栏之案列

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《产品经理如何画泳道图&流程图》 ⛺️ 越努力 &#xff0c;越幸运 目录 一、轮播图简介 1、什么是轮播图 2、轮播图有什么作用 3、轮播图有什么特点 4、轮播图适应范围 5、…

idea中定时+多数据源配置

因项目要求,需要定时从达梦数据库中取数据,并插入或更新到ORACLE数据库中 1.pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-…

PTA:分段统计

题干 给定各个分段统计的区间端点和待统计的一组数据&#xff0c;请统计每区间中数据的个数及其占比百分数。 输入格式: 第一行为一个正整数n(0<n<100)&#xff0c;表示分段区间的端点数&#xff0c;n个端点构成n-1个区间。其后的n个整数按升序给出&#xff0c;分别表示…