让我们用 JavaScript 创建一个轻量级的原生事件总线

Avatar of Carter Li
Carter Li

DigitalOcean 提供适合您旅程每个阶段的云产品。从 价值 200 美元的免费积分 开始!

事件总线是一种设计模式(虽然我们在这里讨论的是 JavaScript,但它在任何语言中都是一种设计模式),可以用来简化不同组件之间的通信。它也可以被认为是发布/订阅或 pubsub

其理念是,组件可以监听事件总线,以了解何时执行其操作。例如,“选项卡面板”组件可能会监听告诉它更改活动选项卡的事件。当然,这可能是由点击选项卡之一触发的,因此完全在该组件内处理。但是,使用事件总线,其他一些元素可以告诉选项卡更改。想象一个表单提交导致用户需要在特定选项卡中收到提醒的错误,因此表单向事件总线发送消息,告诉选项卡组件更改活动选项卡到包含错误的选项卡。这就是它在事件总线上的样子。

该情况的伪代码如下所示…

// Tab Component
Tabs.changeTab = id => {
  // DOM work to change the active tab.
}
MyEventBus.subscribe("change-tab", Tabs.changeTab(id));

// Some other component...
// something happens, then:
MyEventBus.publish("change-tab", 2);  

您需要一个 JavaScript 库来做到这一点吗?(技巧问题:您 永远 不需要 JavaScript 库)。好吧,有很多选择

另外,请查看 Mitt,这是一个 gzip 后仅 200 字节的库。这种简单的模式似乎激发了人们以尽可能简洁的方式自己解决它。

让我们自己做吧!我们根本不会使用任何第三方库,而是利用 JavaScript 中已经内置的事件监听系统,也就是我们所知且喜爱的 addEventListener

首先,一点背景

JavaScript 中的 addEventListener API 是 EventTarget 类的成员函数。我们之所以可以将 click 事件绑定到按钮,是因为 <button>HTMLButtonElement)的原型接口间接继承自 EventTarget

来源:MDN Web 文档

与大多数其他 DOM 接口不同,EventTarget 可以使用 new 关键字 直接 创建。它在所有现代浏览器中都受支持,但只是 最近才出现。正如我们在上面的屏幕截图中看到的那样,Node 继承了 EventTarget,因此所有 DOM 节点都有方法 addEventListener

诀窍在这里

我建议使用一种极其轻量级的 Node 类型来充当我们的事件监听总线:HTML 注释(<!-- comment -->

对于浏览器渲染引擎来说,HTML 注释只是代码中的注释,除了为开发人员提供描述性文本之外没有其他功能。但由于注释仍然是用 HTML 编写的,因此它们最终会以真实节点的形式出现在 DOM 中,并具有自己的原型接口——Comment——它继承自 Node

Comment可以直接从 new 创建,就像 EventTarget 一样

const myEventBus = new Comment('my-event-bus');

我们还可以使用古老但广泛支持的 document.createComment API。它需要一个 data 参数,即注释的内容。它甚至可以是空字符串

const myEventBus = document.createComment('my-event-bus');

现在我们可以使用 dispatchEvent 发射事件,它接受一个 Event 对象。要传递用户定义的事件数据,请使用 CustomEvent,其中 detail 字段可以用于包含任何数据。

myEventBus.dispatchEvent(
  new CustomEvent('event-name', { 
    detail: 'event-data'
  })
);

Internet Explorer 9-11 支持 CustomEvent,但没有一个版本支持 new CustomEvent。使用 document.createEvent 模拟它很复杂,因此如果您需要 IE 支持,则有一种方法可以 为它添加填充

现在我们可以绑定事件监听器了

myEventBus.addEventListener('event-name', ({ detail }) => {
  console.log(detail); // => event-data
});

如果一个事件只打算触发一次,我们可以对 一次性绑定 使用 { once: true }。其他选项不适合这里。要删除事件监听器,可以使用原生的 removeEventListener

调试

绑定到单个事件总线的事件数量可能非常大。如果您忘记删除事件,还会出现内存泄漏。如果我们想知道有多少个事件绑定到 myEventBus 呢?

myEventBus 是一个 DOM 节点,因此可以在浏览器的 DevTools 中对其进行检查。从那里,我们可以在元素 → 事件监听器选项卡中找到事件。请确保取消选中“祖先”,以隐藏绑定到 documentwindow 的事件。

示例

一个缺点是 EventTarget 的语法略显冗长。我们可以为它编写一个简单的包装器。下面是 TypeScript 中的一个演示

class EventBus<DetailType = any> {
  private eventTarget: EventTarget;
  constructor(description = '') { this.eventTarget = document.appendChild(document.createComment(description)); }
  on(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener); }
  once(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener, { once: true }); }
  off(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.removeEventListener(type, listener); }
  emit(type: string, detail?: DetailType) { return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail })); }
}
    
// Usage
const myEventBus = new EventBus<string>('my-event-bus');
myEventBus.on('event-name', ({ detail }) => {
  console.log(detail);
});

myEventBus.once('event-name', ({ detail }) => {
  console.log(detail);
});

myEventBus.emit('event-name', 'Hello'); // => Hello Hello
myEventBus.emit('event-name', 'World'); // => World

以下演示提供了编译后的 JavaScript。


就这样!我们刚刚创建了一个无依赖的事件监听总线,其中一个组件可以通知另一个组件发生更改,以触发操作。执行此类操作不需要完整的库,它打开的可能性是无限的。