事件总线是一种设计模式(虽然我们在这里讨论的是 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 库)。好吧,有很多选择
- PubSubJS
- EventEmitter3
- Postal.js
- jQuery 甚至 支持自定义事件,这与该模式高度相关。
另外,请查看 Mitt,这是一个 gzip 后仅 200 字节的库。这种简单的模式似乎激发了人们以尽可能简洁的方式自己解决它。
让我们自己做吧!我们根本不会使用任何第三方库,而是利用 JavaScript 中已经内置的事件监听系统,也就是我们所知且喜爱的 addEventListener
。
首先,一点背景
JavaScript 中的 addEventListener
API 是 EventTarget
类的成员函数。我们之所以可以将 click
事件绑定到按钮,是因为 <button>
(HTMLButtonElement
)的原型接口间接继承自 EventTarget
。

与大多数其他 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 中对其进行检查。从那里,我们可以在元素 → 事件监听器选项卡中找到事件。请确保取消选中“祖先”,以隐藏绑定到 document
和 window
的事件。

示例
一个缺点是 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。
就这样!我们刚刚创建了一个无依赖的事件监听总线,其中一个组件可以通知另一个组件发生更改,以触发操作。执行此类操作不需要完整的库,它打开的可能性是无限的。
酷 :)
在选择这种方法之前,请考虑另外两种选择
直接使用 window 对象,它已经存在。您的包装器没有添加任何新功能,如果您还不熟悉标准浏览器 API,现在是学习的好时机。使用 window 对象可以使监听器在发射事件的脚本甚至加载之前进行注册——而您的包装器成为订阅者必须加载或以某种方式定位的新依赖项。使用您的供应商名称作为事件名称前缀,以确保您不会与可能使用相同总线的第三方脚本冲突。
如果您面向 Node,在那里已经提供了流行的此类软件包,而您仍然坚持要自己开发,那么使用标准的 Map 和 Set 类来存储监听器,只需要几行代码。这将适用于在浏览器之外运行 JavaScript 的任何内容。
如果您这样做是为了“更短的方法名”,那么您这样做是出于错误的原因——如果您为浏览器开发,几乎肯定需要使用原生事件功能,并且通过创建具有不一致名称的相同功能,并不会有什么好处。
如果您只针对浏览器,那么您要寻找的功能已经存在,并且完全标准化。当我们对这些事情发表意见时,我们只是在为下一位必须维护该产品的人创造无用的复杂性和额外的学习曲线。
如果可以选择,始终追求简单。
您用作事件总线的 DOM 应该保持干净,我不认为使用供应商前缀是个好主意,因为它冗长,并且仍然有可能与其他事件冲突
IMO,事件总线 DOM 本身不应该发射原生事件,不应该对 DOM 树有任何影响,也不应该被其他第三方库重复使用。
使用 window 是 IMO 最糟糕的选择,因为页面中发射的每个事件都将被冒泡到 window,并且有大量的第三方库在 window 上绑定事件,这可能会出现问题。
您可以将事件总线视为 WordPress 中的钩子。这个事件总线使用原生 JavaScript,但不使用 Event API,因此它支持更多浏览器。适合在小型应用程序中扩展
https://github.com/taufik-nurrohman/hook
我不明白为什么创建注释节点而不是使用组件的“容器”,因为它已经存在了?
因为它们可以发射原生事件,这可能会干扰其他订阅者
除了向后兼容 IE 之外,使用“new Comment”(或任何其他 DOM 元素)和通过
new EventTarget()
直接创建新的 EventTarget 实例有什么区别?我还没有尝试,但很快就会尝试。
我认为使用注释作为事件总线的想法很疯狂也很有趣。我之前从未想过,所以感谢你的分享。今天我学到了新东西。
因为注释可以被浏览器开发者工具检查,这对于调试很有用。
参见调试部分
我一直使用空
div
来实现相同的技术。感谢你详细地分享了这一点。现在我知道我可以使用一个裸 EventTarget 实例来实现更轻量级的功能。(我不需要支持 IE11。)我喜欢你的系列。
但是,为什么标题是“……JavaScript 中的原生事件总线”,但你最终却使用了 TypeScript?
JavaScript 类示例是什么?
嗯,TypeScript 通常是带有类型的 JavaScript。只需删除这些类型断言,你就可以获得 JavaScript。
使用 DOM 注释来利用 EventTarget 是不必要的。这是我关于 EventBus 的建议
非常感谢你分享扩展 EventTarget 的代码。我认为作者说要使用 DOM 注释是因为 EventTarget 的浏览器支持不足。
这正是我一直在寻找的东西。我是一名专注于数据的软件工程师,刚接触 TypeScript 开发。我有一个问题。有谁知道为什么这不能用于连接 Syncfusion 或 Telerik 等第三方库?我在启用跨组件方法调用方面遇到了很大的困难。
off 方法似乎不起作用。
我真的很喜欢使用注释作为事件总线的想法,因为你可以确保它不会干扰任何布局。但是,如何在 JavaScript 中选择此注释呢?