建个企业网站多少钱,上海市工程咨询行业协会,网站关键词效果追踪怎么做,高速访问免费全自动网页制作系统跨组件通信和数据共享不是一件容易的事#xff0c;如果通过 prop 一层层传递#xff0c;太繁琐#xff0c;而且仅适用于从上到下的数据传递#xff1b;建立一个全局的状态 Store#xff0c;每个数据可能两三个组件间需要使用#xff0c;其他地方用不着#xff0c;挂那么…跨组件通信和数据共享不是一件容易的事如果通过 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 Mapstring, 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?: PartialEventEmitterConfig
): EventEmitter;
function useEmitter(config: PartialEventEmitterConfig): EventEmitter;
function useEmitterM {}(name?: string,initialEventName?: string,// ts-ignoreinitialListener?: (...args: M[typeof initialEventName][]) void,config?: PartialEventEmitterConfig
): EventEmitter;// ts-ignore
function useEmitterM {}(nameOrConfig?: string | PartialEventEmitterConfig,initialEventNameOrConfig?: string | PartialEventEmitterConfig,// ts-ignoreinitialListener?: (...args: M[typeof initialEventNameOrConfig][]) void,config?: PartialEventEmitterConfig
) {const globalListeners useContext(GlobalListenersContext);// 根据参数类型确定实际的参数值let configActual: PartialEventEmitterConfig {};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: PrettifyEventReceiverOptions
): [any[] | null, EventReceiver];function useReceiver(eventNameOrOptions: string | PrettifyEventReceiverOptions,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] useStateany[] | 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 useProvideT any(name: string,state: T,setState?: DispatchSetStateActionT
) {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 useInjectT 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, DispatchSetStateActionV] {// 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] useStatePerson({name: Evan,age: 20,});useProvide(someone, state);return (ButtononClick{() setState({ ...state, name: state.name Evan ? Nave : Evan })}{state.name}/ButtonButton 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}/spandiv style{{ width: 2rem }}/divspan{state?.age}/span/divdiv style{{ display: flex }}span{state2?.name}/spandiv style{{ width: 2rem }}/divspan{state2?.age}/span/div/);
};const View () {return (h4UseProvide/h4UseProvideExample /h4Inject/h4UseInjectExample //);
};Demo 效果图 Bingo! 用于跨组件协同的 useProvide 和 useInject 就这样实现了 (PS : 我这里的 useProvide 和 useInject 并没有开发命名空间你们可以拓展参数来提供更细粒度的数据隔离)