JS `EventTarget` 与 `CustomEvent` 实现高性能事件总线

各位观众,晚上好!我是你们的老朋友,今天咱们聊聊JS里的“秘密武器”——EventTargetCustomEvent,教大家怎么用它们打造一个高性能的事件总线,让你的代码像开了挂一样流畅!

一、事件总线:代码界的“顺丰快递”

想象一下,你的代码是一座城市,各个模块是不同的建筑。如果这些建筑之间需要交流信息,最笨的办法就是挨家挨户送信,效率低到爆炸。这时候,就需要一个“事件总线”,相当于城市里的“顺丰快递”,专门负责传递消息,让各个模块之间解耦,互不干扰。

事件总线,简单来说,就是一个发布/订阅系统。模块A想告诉模块B发生了什么,它就往事件总线上“发布”一个事件。模块B提前订阅了这类事件,一旦事件总线收到这个事件,就会通知模块B。这样,A和B之间就完成了通信,而不需要直接相互依赖。

二、EventTarget:事件总线的“地基”

EventTarget 是一个内置的JS接口,提供了三个关键方法:

  • addEventListener(type, listener):监听特定类型的事件。
  • removeEventListener(type, listener):移除特定类型的事件监听器。
  • dispatchEvent(event):触发一个事件。

你可以把 EventTarget 看作是一个事件管理的“地基”,所有需要支持事件机制的对象都可以继承它。可惜的是,原生的EventTarget并非类,所以不能直接 extends,需要一些小技巧。

三、CustomEvent:事件总线的“包裹”

CustomEvent 继承自 Event,允许你创建自定义的事件。它有一个重要的属性:detail,可以用来传递事件相关的数据,就像快递包裹里装的东西一样。

例如,你可以创建一个名为 userLoggedIn 的自定义事件,并在 detail 属性中包含用户的ID和用户名:

const user = { id: 123, name: "张三" };
const event = new CustomEvent("userLoggedIn", { detail: user });

四、打造一个高性能事件总线:实战演练

现在,咱们就用 EventTargetCustomEvent 来搭建一个高性能的事件总线。

1. 创建一个事件总线类

首先,我们需要一个类来管理事件的注册和触发。为了使用 EventTarget 的功能,我们需要手动实现它的方法。

class EventBus {
  constructor() {
    this._listeners = {}; // 使用对象存储事件监听器
  }

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

  removeEventListener(type, listener) {
    if (!this._listeners[type]) {
      return;
    }

    this._listeners[type] = this._listeners[type].filter(l => l !== listener);

    if (this._listeners[type].length === 0) {
      delete this._listeners[type];
    }
  }

  dispatchEvent(event) {
    if (typeof event === 'string') {
        event = new CustomEvent(event);
    }
    const type = event.type;
    if (!this._listeners[type]) {
      return;
    }

    this._listeners[type].forEach(listener => {
      listener.call(this, event);
    });
  }

  // 为了更方便,我们可以添加一些别名方法
  on(type, listener) {
    this.addEventListener(type, listener);
  }

  off(type, listener) {
    this.removeEventListener(type, listener);
  }

  emit(type, detail) {
    const event = new CustomEvent(type, { detail });
    this.dispatchEvent(event);
  }
}

// 创建一个全局的事件总线实例
const eventBus = new EventBus();

这个 EventBus 类就像一个“事件调度中心”,负责管理所有的事件监听器和事件触发。 它使用一个对象 _listeners 来存储事件监听器,键是事件类型,值是监听器数组。

2. 模块之间的通信

现在,假设我们有两个模块:ModuleAModuleB

  • ModuleA 负责用户登录,登录成功后需要通知 ModuleB
  • ModuleB 负责显示用户的信息。
// ModuleA
const ModuleA = {
  login(username, password) {
    // 模拟登录
    setTimeout(() => {
      const user = { id: 123, name: username };
      eventBus.emit("userLoggedIn", user); // 发布 userLoggedIn 事件
      console.log("ModuleA: 用户登录成功");
    }, 1000);
  },
};

// ModuleB
const ModuleB = {
  init() {
    eventBus.on("userLoggedIn", (event) => {
      const user = event.detail;
      this.displayUserInfo(user);
    });
  },

  displayUserInfo(user) {
    console.log(`ModuleB: 欢迎 ${user.name} (ID: ${user.id})`);
  },
};

// 初始化 ModuleB
ModuleB.init();

// 模拟用户登录
ModuleA.login("李四", "password123");

在这个例子中,ModuleA 在登录成功后,调用 eventBus.emit("userLoggedIn", user) 发布了一个 userLoggedIn 事件,并将用户信息作为 detail 属性传递。

ModuleB 在初始化时,通过 eventBus.on("userLoggedIn", ...) 监听了 userLoggedIn 事件。当事件发生时,ModuleB 的回调函数会被执行,并从 event.detail 中获取用户信息,然后显示出来。

3. 优化事件总线:性能至上

虽然上面的代码可以工作,但是还有一些可以优化的地方,以提高事件总线的性能。

  • 避免全局事件总线: 全局事件总线可能会导致命名冲突和难以调试的问题。最好为不同的模块或组件创建独立的事件总线实例。
  • 使用弱引用: 如果监听器是一个对象的方法,并且对象被销毁了,但是监听器仍然存在于事件总线中,就会导致内存泄漏。可以使用 WeakRef 来解决这个问题。
  • 限制事件类型: 如果事件类型过多,会增加事件总线的复杂度。最好限制事件类型的数量,并使用枚举或常量来定义事件类型。
  • 避免过度使用: 事件总线虽然强大,但是也不是万能的。过度使用事件总线可能会导致代码难以理解和维护。

4.更高级的用法:通配符事件

有时候,我们可能需要监听一类事件,而不是具体的某个事件。例如,监听所有以 user. 开头的事件。这可以使用通配符来实现。

class EventBus {
  constructor() {
    this._listeners = {};
  }

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

  removeEventListener(type, listener) {
    for (const type in this._listeners) {
      if (this._listeners.hasOwnProperty(type)) {
        this._listeners[type] = this._listeners[type].filter(l => l !== listener);
        if (this._listeners[type].length === 0) {
          delete this._listeners[type];
        }
      }
    }
  }

  dispatchEvent(event) {
    if (typeof event === 'string') {
        event = new CustomEvent(event);
    }
    const type = event.type;

    for (const listenerType in this._listeners) {
      if (this._listeners.hasOwnProperty(listenerType)) {
        // 匹配通配符事件
        const regex = new RegExp(`^${listenerType.replace(/*/g, '.*')}$`);
        if (regex.test(type)) {
          this._listeners[listenerType].forEach(listener => {
            listener.call(this, event);
          });
        }
      }
    }
  }

  on(type, listener) {
    this.addEventListener(type, listener);
  }

  off(type, listener) {
    this.removeEventListener(type, listener);
  }

  emit(type, detail) {
    const event = new CustomEvent(type, { detail });
    this.dispatchEvent(event);
  }
}

const eventBus = new EventBus();

eventBus.on("user.*", (event) => {
  console.log("收到用户相关事件:", event.type, event.detail);
});

eventBus.emit("user.loggedIn", { id: 1, name: "王五" });
eventBus.emit("user.loggedOut", { id: 1, name: "王五" });
eventBus.emit("product.created", { id: 1, name: "商品A" }); // 这个事件不会被监听到

在这个例子中,我们使用 user.* 作为事件类型,表示监听所有以 user. 开头的事件。在 dispatchEvent 方法中,我们使用正则表达式来匹配事件类型,如果匹配成功,就执行相应的监听器。

五、事件总线的替代方案

虽然事件总线是一个很有用的工具,但是也有一些替代方案可以考虑:

  • 观察者模式: 观察者模式是一种更简单的发布/订阅模式,适用于模块之间的关系比较简单的情况。
  • 状态管理库: 如果你的应用程序使用了状态管理库(如 Redux 或 Vuex),你可以使用状态管理库提供的事件机制来实现模块之间的通信。
  • 依赖注入: 依赖注入可以帮助你解耦模块之间的依赖关系,从而减少对事件总线的需求。

六、总结

EventTargetCustomEvent 是JS中强大的事件处理工具,可以用来构建高性能的事件总线,实现模块之间的解耦和通信。

特性 描述 优点 缺点
EventTarget 提供了 addEventListener, removeEventListener, dispatchEvent 方法,是实现事件机制的基础。 内置接口,无需额外依赖,轻量级。 需要手动实现类和方法,不能直接继承。
CustomEvent 允许创建自定义事件,并可以传递数据。 可以携带任意数据,灵活性高。 需要手动创建事件,稍微繁琐。
事件总线 基于 EventTargetCustomEvent 实现的发布/订阅系统。 解耦模块之间的依赖关系,提高代码的可维护性和可测试性,方便模块之间的通信。 可能导致过度耦合,事件类型过多会导致混乱,需要谨慎使用。
通配符事件 允许监听一类事件,而不是具体的某个事件。 更加灵活,可以监听多个事件。 实现稍微复杂,需要使用正则表达式。
替代方案 观察者模式、状态管理库、依赖注入。 不同的方案适用于不同的场景,可以根据实际情况选择。 每种方案都有其优缺点,需要根据实际情况权衡。

希望今天的讲解对大家有所帮助。记住,代码的世界没有银弹,只有最适合你的解决方案。 灵活运用 EventTargetCustomEvent,让你的代码更加优雅高效!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注