likes
comments
collection
share

JavaScript 发布-订阅设计模式实现 React EventBus

作者站长头像
站长
· 阅读数 80

原理

EventEmitter主要是使用了发布订阅JavaScript设计模式,它的流程如下:

JavaScript 发布-订阅设计模式实现 React EventBus

  1. 消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者。
  2. 订阅者:去消息中心订阅自己感兴趣的消息。
  3. 发布者:满足条件时,通过消息中心发布消息。

知道了原理,我们自己来实现一个发布订阅模式,再去实现EventEmitter就很简单了。

class PubSub {
  constructor() {
    // 一个对象存放所有的消息订阅
    // 每个消息对应一个数组,数组结构如下
    // {
    //   "event1": [cb1, cb2]
    // }
    this.events = {};
  }

  subscribe(event, callback) {
    if (this.events[event]) {
      // 如果有人订阅过了,这个键已经存在,就往里面加就好了
      this.events[event].push(callback);
    } else {
      // 没人订阅过,就建一个数组,回调放进去
      this.events[event] = [callback];
    }
  }

  publish(event, ...args) {
    // 取出所有订阅者的回调执行
    const subscribedEvents = this.events[event];

    if (subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach((callback) => {
        callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {
    // 删除某个订阅,保留其他订阅
    const subscribedEvents = this.events[event];

    if (subscribedEvents && subscribedEvents.length) {
      this.events[event] = this.events[event].filter((cb) => cb !== callback);
    }
  }
}

场景

EventEmitter很适合在不修改组件状态结构的情况下进行组件通信,然而它的生命周期不受React管理,需要手动添加/清理监听事件很麻烦。而且,如果一个EventEmitter没有使用就被初始化也会有点麻烦。

开发过 Vue 的同学应该都知道 Vue事件总线(EventBus),其实 EventEmitter 和它的实现方式都是大同小异。

目的

所以使用React Context结合EventEmitter的目的便是

  • 添加高阶组件,通过React Context为所有子组件注入em对象。
  • 添加自定义hooks,从React Context获取emitter对象,并暴露出合适的函数。
  • 自动清理emitter对象和emitter listener

实现

实现基本的EventEmitter

export type BaseEvents = Record<string, any[]>;

/**
 * 事件总线
 * 实际上就是发布订阅模式的一种简单实现
 * 类型定义受到 {@link https://github.com/andywer/typed-emitter/blob/master/index.d.ts} 的启发,不过只需要声明参数就好了,而不需要返回值(应该是 {@code void})
 */
export class EventEmitter<Events extends BaseEvents> {
  // 一个对象存放所有的消息订阅
  private readonly events = new Map<keyof Events, Function[]>();

  /**
   * 添加一个事件监听程序
   * @param type 监听类型
   * @param callback 处理回调
   * @returns {@code this}
   */
  add<E extends keyof Events>(type: E, callback: (...args: Events[E]) => void) {
    const callbacks = this.events.get(type) || [];
    callbacks.push(callback);
    this.events.set(type, callbacks);
  }

  /**
   * 移除一个事件监听程序
   * @param type 监听类型
   * @param callback 处理回调
   * @returns {@code this}
   */
  remove<E extends keyof Events>(
    type: E,
    callback: (...args: Events[E]) => void
  ) {
    const callbacks = this.events.get(type) || [];
    this.events.set(
      type,
      callbacks.filter((fn: any) => fn !== callback)
    );
  }

  /**
   * 移除一类事件监听程序
   * @param type 监听类型
   * @returns {@code this}
   */
  removeByType<E extends keyof Events>(type: E) {
    this.events.delete(type);
  }

  /**
   * 触发一类事件监听程序
   * @param type 监听类型
   * @param args 处理回调需要的参数
   * @returns {@code this}
   */
  emit<E extends keyof Events>(type: E, ...args: Events[E]) {
    const callbacks = this.events.get(type) || [];
    callbacks.forEach((fn) => fn(...args));
  }

  /**
   * 获取一类事件监听程序
   * @param type 监听类型
   * @returns 一个只读的数组,如果找不到,则返回空数组 {@code []}
   */
  listeners<E extends keyof Events>(type: E) {
    // Object.freeze() 方法可以冻结一个对象。
    // 一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
    return Object.freeze(this.events.get(type) || []);
  }
}

结合 Context 实现一个包裹组件

import { createContext, PropsWithChildren } from "react";
import { BaseEvents, EventEmitter } from "@/utils/EventEmitter";

export const EventEmitterContext = createContext<EventEmitter<any>>(
  null as any
);

export function EventEmitterRC<T extends BaseEvents>(
  props: PropsWithChildren<{ value: EventEmitter<T> }>
) {
  const { value, children } = props;

  return (
    <EventEmitterContext.Provider value={value}>
      {children}
    </EventEmitterContext.Provider>
  );
}

使用 hooks 暴露 emitter api

我们主要暴露的 API 只有三个

  • useListener:添加监听器,使用hooks是为了能在组件卸载时自动清理监听函数。
  • emit:触发监听器,直接调用即可。
  • emitter:在当前组件树生效的emitter对象。
import {
  useContext,
  useCallback,
  useMemo,
  DependencyList,
  useEffect,
} from "react";
import { EventEmitterContext } from "@/layout/EventEmitterRC";
import { BaseEvents, EventEmitter } from "@/utils/EventEmitter";

function useEmit<Events extends BaseEvents>() {
  const em = useContext(EventEmitterContext);

  return useCallback(
    <E extends keyof Events>(type: E, ...args: Events[E]) =>
      em.emit(type, ...args),
    [em]
  );
}

export function useEventEmitter<Events extends BaseEvents>() {
  const emit = useEmit<Events>();
  // 这里使用 useMemo 产生的 emitter 对象的原因是在当前组件树 emitter 仅初始化一次
  const emitter = useMemo(() => new EventEmitter<Events>(), []);

  return {
    useListener: <E extends keyof Events>(
      type: E,
      listener: (...args: Events[E]) => void,
      deps: DependencyList = []
    ) => {
      const em = useContext(EventEmitterContext);

      useEffect(() => {
        em.add(type, listener);

        return () => em.remove(type, listener);
      }, [listener, type, ...deps]);
    },
    emit,
    emitter,
  };
}

使用

使用起来非常简单,在需要使用的emitter hooks的组件外部包裹一个 EventEmitterRC 组件,然后就可以使用 useEventEmitter 了。 下面是一个简单的Todo示例,使用emitter实现了Todo表单与Todo列表之间的通信。 目录结构如下

  • todo
    • component
      • TodoForm.tsx
      • TodoList.tsx
    • modal
      • TodoEntity.ts
      • TodoEvents.ts
    • Todo.tsx Todo父组件,使用EventEmitterRC包裹子组件
import { EventEmitterRC } from "@/layout/EventEmitterRC";
import { useEventEmitter } from "@/hooks/useEventEmitter";

const Todo = () => {
  const { emitter } = useEventEmitter();
  
  return (
    <EventEmitterRC value={emitter}>
      <TodoForm />
      <TodoList />
    </EventEmitterRC>
  );
};

在表单组件中使用 useEventEmitter hook获得 emit 方法,然后在添加todo时触发它。

import { useState, FormEvent } from "react";
import { useEventEmitter } from "@/hooks/useEventEmitter";
import { TodoEvents } from "../modal/TodoEvents";

const TodoForm = () => {
  const { emit } = useEventEmitter<TodoEvents>();
  const [title, setTitle] = useState("");

  function handleAddTodo(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();

    emit("addTodo", { title });
    setTitle("");
  }

  return (
    <form onSubmit={handleAddTodo}>
      <label htmlFor={"title"}>标题:</label>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        id={"title"}
      />
      <button type={"submit"}>添加</button>
    </form>
  );
};

export default TodoForm;

在列表组件中使用 useEventEmitter hooks获得 useListener hooks,然后监听添加todo的事件。

import { useState } from "react";
import { useEventEmitter } from "@/hooks/useEventEmitter";
import { TodoEntity } from "../modal/TodoEntity";
import { TodoEvents } from "../modal/TodoEvents";

const TodoList = () => {
  const [list, setList] = useState<TodoEntity[]>([]);
  const { useListener } = useEventEmitter<TodoEvents>();

  useListener("addTodo", (todo) => setList([...list, todo]), [list]);

  return (
    <ul>
      {list.map((todo, i) => (
        <li key={i}>{todo.title}</li>
      ))}
    </ul>
  );
};

export default TodoList;

下面是相关TypeScript类型

export interface TodoEntity {
  title: string;
}
import { BaseEvents } from "@/utils/EventEmitter";
import { TodoEntity } from "./TodoEntity";

export interface TodoEvents extends BaseEvents {
  addTodo: [entity: TodoEntity];
}

参考文章: 使用 React Context 结合 EventEmitter 从发布订阅模式入手读懂Node.js的EventEmitter源码

转载自:https://juejin.cn/post/7085566957159710750
评论
请登录