An event bus is a design pattern (and while we’ll be talking about JavaScript here, it’s a design pattern in any language) that can be used to simplify communications between different components. It can also be thought of as publish/subscribe or pubsub.
The idea is that components can listen to the event bus to know when to do the things they do. For example, a “tab panel” component might listen for events telling it to change the active tab. Sure, that might happen from a click on one of the tabs, and thus handled entirely within that component. But with an event bus, some other elements could tell the tab to change. Imagine a form submission which causes an error that the user needs to be alerted to within a specific tab, so the form sends a message to the event bus telling the tabs component to change the active tab to the one with the error. That’s what it looks like aboard an event bus.
Pseudo-code for that situation would be like…
// 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);
Do you need a JavaScript library to this? (Trick question: you never need a JavaScript library). Well, there are lots of options out there:
- PubSubJS
- EventEmitter3
- Postal.js
- jQuery even supported custom events, which is highly related to this pattern.
Also, check out Mitt which is a library that’s only 200 bytes gzipped. There is something about this simple pattern that inspires people to tackle it themselves in the most succincet way possible.
Let’s do that ourselves! We’ll use no third-party library at all and leverage an event listening system that is already built into JavaScript with the addEventListener
we all know and love.
First, a little context
The addEventListener
API in JavaScript is a member function of the EventTarget
class. The reason we can bind a click
event to a button is because the prototype interface of <button>
(HTMLButtonElement
) inherits from EventTarget
indirectly.
Different from most other DOM interfaces, EventTarget
can be created directly using the new
keyword. It is supported in all modern browsers, but only fairly recently. As we can see in the screenshot above, Node
inherits EventTarget
, thus all DOM nodes have method addEventListener
.
Here’s the trick
I’m suggesting an extremely lightweight Node
type to act as our event-listening bus: an HTML comment (<!--
comment
-->
).
To a browser rendering engine, HTML comments are just notes in the code that have no functionality other than descriptive text for developers. But since comments are still written in HTML, they end up in the DOM as real nodes and have their own prototype interface—Comment
—which inherits Node
.
The Comment
class can be created from new
directly like EventTarget
can:
const myEventBus = new Comment('my-event-bus');
We could also use the ancient, but widely-supported document.createComment
API. It requires a data
parameter, which is the content of the comment. It can even be an empty string:
const myEventBus = document.createComment('my-event-bus');
Now we can emit events using dispatchEvent
, which accepts an Event
Object. To pass user-defined event data, use CustomEvent
, where the detail
field can be used to contain any data.
myEventBus.dispatchEvent(
new CustomEvent('event-name', {
detail: 'event-data'
})
);
Internet Explorer 9-11 supports CustomEvent
, but none of the versions support new CustomEvent
. It’s complex to simulate it using document.createEvent
, so if IE support is important to you, there’s a way to polyfill it.
Now we can bind event listeners:
myEventBus.addEventListener('event-name', ({ detail }) => {
console.log(detail); // => event-data
});
If an event intends to be triggered only once, we may use { once: true }
for one-time binding. Other options won’t fit here. To remove event listeners, we can use the native removeEventListener
.
Debugging
The number of events bound to single event bus can be huge. There also can be memory leaks if you forget to remove them. What if we want to know how many events are bound to myEventBus
?
myEventBus
is a DOM node, so it can be inspected by DevTools in the browser. From there, we can find the events in the Elements → Event Listeners tab. Be sure to uncheck “Ancestors” to hide events bound on document
and window
.
An example
One drawback is that the syntax of EventTarget
is slightly verbose. We can write a simple wrapper for it. Here is a demo in TypeScript below:
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
And there we have it! We just created a dependency-free event-listening bus where one component can inform another component of changes to trigger an action. It doesn’t take a full library to do this sort of stuff, and the possibilities it opens up are pretty endless.
Source: CSS-tricks.com