Skip to content

全局事件总线

相关库和框架

除了Redux、MobX、Recoil 和 Zustand 这些常见的状态管理库外,还有一些库和框架可以提供类似于全局事件总线(类似于前面提到的 "bus")的功能。以下是一些可以实现类似功能的库和框架:

  1. EventEmitter:

    • EventEmitter 是 Node.js 中的一个模块,用于处理事件和触发事件。它可以在浏览器端和服务端使用。
    • 可以使用 EventEmitter 来实现简单的事件总线,用于在应用程序中进行事件通信。
    • GitHub 地址:EventEmitter
  2. mitt:

    • mitt 是一个小巧且高效的事件总线库,它提供了简单的 API 来处理事件的订阅和发布。
    • mitt 支持事件的命名空间,可以更好地组织和管理事件。
    • GitHub 地址:mitt
  3. PubSubJS:

    • PubSubJS 是一个基于发布/订阅模式的库,可以用于解耦组件之间的通信。
    • 它提供了灵活的 API 来订阅和发布消息,支持多种模式的消息传递。
    • GitHub 地址:PubSubJS
  4. NanoEvents:

    • NanoEvents 是一个极简的事件总线库,大小仅有几行代码,适用于简单的事件管理需求。
    • 它提供了基本的事件订阅和发布功能,适用于小型项目或快速原型开发。
    • GitHub 地址:NanoEvents

这些库都提供了简单且灵活的方式来实现事件总线的功能,可以用于在应用程序中实现类似于全局事件总线的功能。你可以根据项目的需求选择适合的库来管理事件通信。

对比分析

下面是对 EventEmitter、PubSubJS 和 NanoEvents 这几个事件管理库的优缺点综合比较:

EventEmitter

优点:

  1. 内置 Node.js 核心模块: EventEmitter 是 Node.js 核心模块之一,因此在 Node.js 环境中无需额外安装即可使用。
  2. 简单易用: 提供了简单的 API,易于理解和使用。
  3. 支持多个监听器: 可以注册多个监听器来处理同一事件。

缺点:

  1. 仅限于 Node.js: 主要用于 Node.js 环境,不适用于浏览器端开发。
  2. 较为基础: 功能相对简单,不提供一些高级功能,如异步事件处理等。

PubSubJS

优点:

  1. 跨平台: 可以在浏览器端和 Node.js 环境中使用。
  2. 支持模式匹配: 可以使用通配符来订阅多个事件。
  3. 灵活性: 提供了一些高级功能,如取消订阅、模式匹配等。

缺点:

  1. 性能: 在大规模事件处理时性能可能不如其他库。
  2. 功能相对简单: 某些高级功能可能需要自行扩展或结合其他库来实现。

NanoEvents

优点:

  1. 轻量级: 极其轻量级,代码量少,适用于需要简单事件管理的场景。
  2. 现代化: 使用了现代的 ES6+ 特性,易于理解和维护。
  3. 支持浏览器和 Node.js: 可以在浏览器端和 Node.js 环境中使用。

缺点:

  1. 功能相对简单: 不提供一些高级功能,如模式匹配、异步事件处理等。
  2. 社区支持: 相对较新,可能在功能和稳定性上不如一些更成熟的库。

综合来看,选择合适的事件管理库取决于你的项目需求和场景。如果你需要在 Node.js 环境中使用,并且对简单的事件管理足够满足需求,可以考虑 EventEmitter。如果需要跨平台支持,并且希望具有一些高级功能,PubSubJS 可能是一个不错的选择。而对于轻量级的、简单的事件管理需求,NanoEvents 可能是一个很好的选择。

基于mitt自定义实现一个支持once方法的事件管理总线

如果你想基于 mitt 实现一个支持 once 方法的事件管理库,可以通过扩展 mitt 的功能来实现。下面是一个简单的示例代码,展示如何在 mitt 的基础上添加 once 方法:

JavaScript实现

javascript
class CustomEmitter {
  constructor() {
    this.events = new Map();
  }

  on(type, handler) {
    if (!this.events.has(type)) {
      this.events.set(type, []);
    }
    this.events.get(type).push(handler);
  }

  off(type, handler) {
    if (this.events.has(type)) {
      this.events.set(type, this.events.get(type).filter(h => h !== handler));
    }
  }

  once(type, handler) {
    const onceHandler = (...args) => {
      handler(...args);
      this.off(type, onceHandler);
    };
    this.on(type, onceHandler);
  }

  emit(type, ...args) {
    if (this.events.has(type)) {
      this.events.get(type).forEach(handler => {
        handler(...args);
      });
    }
  }
}

// 使用示例
const emitter = new CustomEmitter();

function handleEvent() {
  console.log('Event handled');
}

// 订阅事件,该事件只会执行一次
emitter.once('event', handleEvent);

// 发布事件
emitter.emit('event'); // 输出: Event handled
emitter.emit('event'); // 第二次发布事件,不会再触发 handleEvent

TypeScript实现

typescript
type Handler<T> = (...args: T[]) => void;

class CustomEmitter<T> {
  private events: Map<string, Handler<T>[]> = new Map();

  on(type: string, handler: Handler<T>): void {
    if (!this.events.has(type)) {
      this.events.set(type, []);
    }
    this.events.get(type)!.push(handler);
  }

  off(type: string, handler: Handler<T>): void {
    if (this.events.has(type)) {
      this.events.set(type, this.events.get(type)!.filter(h => h !== handler));
    }
  }

  once(type: string, handler: Handler<T>): void {
    const onceHandler: Handler<T> = (...args: T[]) => {
      handler(...args);
      this.off(type, onceHandler);
    };
    this.on(type, onceHandler);
  }

  emit(type: string, ...args: T[]): void {
    if (this.events.has(type)) {
      this.events.get(type)!.forEach(handler => {
        handler(...args);
      });
    }
  }
}

// 使用示例
const emitter = new CustomEmitter<number>();

function handleEvent(num: number) {
  console.log('Event handled:', num);
}

// 订阅事件,该事件只会执行一次
emitter.once('event', handleEvent);

// 发布事件
emitter.emit('event', 42); // 输出: Event handled: 42
emitter.emit('event', 100); // 第二次发布事件,不会再触发 handleEvent

在这个示例中,我们创建了一个名为 CustomEmitter 的类,实现了 onoffonceemit 方法。once 方法用于订阅事件,该事件只会执行一次,执行后会自动取消订阅。

你可以根据自己的需求进一步扩展这个自定义事件管理库,添加更多功能或优化现有功能。这样的实现可以帮助你更好地理解事件管理库的工作原理。

Web原生API:EventTarget

EventTarget 是 Web API 中的一个接口,代表可以接收事件的目标对象。在浏览器环境中,几乎所有的 DOM 元素都是 EventTarget 的实例,也包括 documentwindow 对象。除了 DOM 元素,你也可以自定义实现 EventTarget 接口,以创建自定义的事件目标对象。

EventTarget 接口包含了以下三个方法:

  1. addEventListener(type, listener, options)

    • 用于在 EventTarget 上注册事件监听器。
    • type 表示事件类型,比如 "click"、"mouseover" 等。
    • listener 是事件处理函数。
    • options 是一个可选的配置对象,可以指定 captureoncepassive 等属性。
  2. removeEventListener(type, listener, options)

    • 用于移除已注册的事件监听器。
    • 参数与 addEventListener 类似,需要提供相同的事件类型、事件处理函数以及可能的配置对象。
  3. dispatchEvent(event)

    • 用于触发一个指定类型的事件,以便让已注册的监听器能够捕获并处理该事件。
    • event 是一个事件对象,可以是 Event 的实例或者其子类的实例。

下面是一个简单的示例,演示了如何在一个自定义的 EventTarget 对象上使用这些方法:

javascript
class CustomEventTarget {
  constructor() {
    this.listeners = {};
  }

  addEventListener(type, listener) {
    if (!(type in this.listeners)) {
      this.listeners[type] = [];
    }
    this.listeners[type].push(listener);
  }

  removeEventListener(type, listener) {
    if (type in this.listeners) {
      this.listeners[type] = this.listeners[type].filter(l => l !== listener);
    }
  }

  dispatchEvent(event) {
    if (event.type in this.listeners) {
      this.listeners[event.type].forEach(listener => {
        listener.call(this, event);
      });
    }
  }
}

// 创建一个自定义事件目标对象
const myEventTarget = new CustomEventTarget();

// 添加事件监听器
function handleEvent(event) {
  console.log('Event handled:', event.type);
}

myEventTarget.addEventListener('click', handleEvent);

// 触发事件
const event = new Event('click');
myEventTarget.dispatchEvent(event);

// 移除事件监听器
myEventTarget.removeEventListener('click', handleEvent);

这个示例展示了如何创建一个简单的自定义事件目标对象 CustomEventTarget,并在其上添加、触发和移除事件监听器。通过理解 EventTarget 接口的使用方式,你可以更好地处理事件相关的逻辑,并实现自定义的事件管理机制。

基于EventTarget结合mitt的实现

基于 EventTarget 结合 mitt 实现一个包含 once 方法的事件总线,可以创建一个自定义的事件总线类,该类内部使用 mitt 来管理事件,并实现 EventTarget 接口的方法。下面是一个示例代码:

typescript
import mitt, { Emitter } from 'mitt';

class EventBus implements EventTarget {
  private emitter: Emitter = mitt();
  
  addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
    this.emitter.on(type, listener as any);
  }

  removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void {
    this.emitter.off(type, callback as any);
  }

  dispatchEvent(event: Event): boolean {
    this.emitter.emit(event.type, event);
    return true;
  }

  once(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
    const onceHandler = (event: Event) => {
      this.removeEventListener(type, onceHandler);
      if (listener) {
        listener(event);
      }
    };
    this.addEventListener(type, onceHandler);
  }
}

// 使用示例
const eventBus = new EventBus();

function handleEvent(event: Event) {
  console.log('Event handled:', event.type);
}

// 订阅事件,该事件只会执行一次
eventBus.once('click', handleEvent);

// 发布事件
const clickEvent = new Event('click');
eventBus.dispatchEvent(clickEvent); // 输出: Event handled: click
eventBus.dispatchEvent(clickEvent); // 第二次发布事件,不会再触发 handleEvent

在这个示例中,我们创建了一个 EventBus 类,实现了 EventTarget 接口的方法,并结合了 mitt 的事件管理功能。通过 addEventListenerremoveEventListenerdispatchEvent 方法,我们使得 EventBus 类可以被视为一个符合 EventTarget 接口的事件目标对象。同时,我们添加了 once 方法,用于订阅一个只会执行一次的事件。

通过这种方式,我们实现了一个包含 once 方法的事件总线,结合了 EventTargetmitt 的功能,使得事件管理更加灵活和强大。

EventEmitter3

在 React 应用中结合 TypeScript 使用 EventEmitter,你可以通过安装适用于浏览器环境的 EventEmitter 库来实现。以下是一个简单的示例,演示了如何在 React 组件中使用 EventEmitter 进行事件的订阅和发布:

首先,你需要安装适用于浏览器环境的 EventEmitter 库。你可以使用 npm 或 yarn 进行安装:

bash
npm install eventemitter3

或者

bash
yarn add eventemitter3

接下来,你可以在 React 组件中使用 EventEmitter。下面是一个示例代码:

typescript
import React, { useEffect } from 'react';
import { EventEmitter } from 'eventemitter3';

// 创建一个全局的 EventEmitter 实例
const emitter = new EventEmitter();

const MyComponent: React.FC = () => {
  useEffect(() => {
    // 订阅事件
    const eventHandler = () => {
      console.log('事件被触发了!');
    };
    emitter.on('event', eventHandler);

    return () => {
      // 在组件卸载时移除监听器
      emitter.removeListener('event', eventHandler);
    };
  }, []);

  const triggerEvent = () => {
    // 触发事件
    emitter.emit('event');
  };

  return (
    <div>
      <button onClick={triggerEvent}>触发事件</button>
    </div>
  );
};

export default MyComponent;

在这个示例中,我们创建了一个全局的 EventEmitter 实例 emitter,并在组件中订阅了名为 'event' 的事件。在组件卸载时移除了事件监听器,以避免内存泄漏。当点击按钮时,会触发 'event' 事件。

这样,你就可以在 React 组件中使用 EventEmitter 进行事件的订阅和发布。请注意,这只是一个简单的示例,你可以根据实际需求扩展和修改代码。

用法示例

在 JavaScript 中,EventEmitter 实例可以在不同模块之间进行跨模块调用。这意味着你可以在一个模块中创建 EventEmitter 实例,并在另一个模块中订阅和触发该实例的事件。

下面是一个简单的示例,演示了如何在不同模块中使用同一个 EventEmitter 实例进行跨模块调用:

模块 1: emitterModule.ts

typescript
import { EventEmitter } from 'eventemitter3';

// 创建一个全局的 EventEmitter 实例
export const emitter = new EventEmitter();

模块 2: moduleA.ts

typescript
import { emitter } from './emitterModule';

// 订阅事件
emitter.on('event', () => {
  console.log('事件在 Module A 中被触发了!');
});

模块 3: moduleB.ts

typescript
import { emitter } from './emitterModule';

// 触发事件
emitter.emit('event');

在这个示例中,emitterModule.ts 模块中创建了一个全局的 EventEmitter 实例 emittermoduleA.ts 模块订阅了 'event' 事件,而 moduleB.ts 模块触发了 'event' 事件。通过共享同一个 EventEmitter 实例,模块之间可以进行跨模块的事件通信。

请注意,为了确保模块之间能够共享同一个 EventEmitter 实例,你需要在每个模块中导入相同的实例。这样,不同模块之间就可以通过该实例进行事件的订阅和发布。

通过这种方式,你可以在不同模块之间实现跨模块的事件通信,使得模块之间更加解耦和灵活。

mitt

mitt 是一个小巧且高效的事件总线库,可以用于处理事件的订阅和发布。它提供了简单的 API 来管理事件,适用于现代 JavaScript 应用程序,包括 React 项目。下面是关于 mitt 的功能特性和用法的详细介绍,以及如何在 React 项目中结合父子、子孙组件使用 mitt。

功能特性

  1. 轻量级:mitt 是一个小巧的库,体积小,性能高效。

  2. 简单易用:提供了简单的 API,包括订阅事件、发布事件和移除事件监听器。

  3. 支持命名空间:可以为事件添加命名空间,更好地组织和管理事件。

用法示例

首先,安装 mitt:

bash
npm install mitt

或者

bash
yarn add mitt

在 React 中使用 mitt

typescript
import React, { useEffect } from 'react';
import mitt, { Emitter } from 'mitt';

// 创建一个全局的事件总线
const emitter: Emitter = mitt();

// 父组件
const ParentComponent: React.FC = () => {
  useEffect(() => {
    // 订阅事件
    const eventHandler = () => {
      console.log('父组件收到事件!');
    };
    emitter.on('event', eventHandler);

    return () => {
      // 在组件卸载时移除监听器
      emitter.off('event', eventHandler);
    };
  }, []);

  return (
    <div>
      <ChildComponent />
    </div>
  );
};

// 子组件
const ChildComponent: React.FC = () => {
  const triggerEvent = () => {
    // 触发事件
    emitter.emit('event');
  };

  return (
    <div>
      <button onClick={triggerEvent}>触发事件</button>
      <GrandChildComponent />
    </div>
  );
};

// 孙子组件
const GrandChildComponent: React.FC = () => {
  useEffect(() => {
    // 订阅事件
    const eventHandler = () => {
      console.log('孙子组件收到事件!');
    };
    emitter.on('event', eventHandler);

    return () => {
      // 在组件卸载时移除监听器
      emitter.off('event', eventHandler);
    };
  }, []);

  return <div>孙子组件</div>;
};

export default ParentComponent;

在这个示例中,我们创建了一个全局的事件总线 emitter,并在父组件、子组件和孙子组件中分别订阅了 'event' 事件。当点击按钮时,子组件触发了 'event' 事件,父组件和孙子组件都会收到事件并执行相应的事件处理函数。

通过这种方式,你可以在 React 项目中使用 mitt 实现父子、子孙组件之间的事件通信。这种方式可以帮助你实现组件之间的解耦,使得代码更加灵活和可维护。