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

发布时间:2023年12月17日

跨组件通信和数据共享不是一件容易的事,如果通过 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 并没有开发命名空间,你们可以拓展参数来提供更细粒度的数据隔离)

文章来源:https://blog.csdn.net/m0_51810668/article/details/135048610
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。