Building a Pub/Sub System in JavaScript: A Simple Implementation
Pub/Sub (Publish-Subscribe) pattern is a famous design pattern. This article presents a complete JavaScript implementation
Core Features
1. Subscription Implementation
2. Publish Event
3. UnSubscribe Implemention
4. Single-Fire Subscriptions
5. Clear all Subscriptions
6. Method Chaining
7. Error Isolation
Implementation Breakdown
1. Storage Architecture
javaScript
class PubSub {
constructor() {
this.events = new Map(); // event -> callback[]
}
}
Here, we use a Map to store registered events. The keys are event names, and the values are arrays of callbacks (one-to-many). Events are added to the store when a subscriber subscribes. When publishing an event, we retrieve its callbacks and execute them sequentially—this is the core idea.
2. Subscription Implementation
javaScript
subscribe(event, callback) {
if (typeof event !== 'string') {
throw new TypeError('Event name must be a string');
}
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}
const callbacks = this.events.get(event) || [];
callbacks.push(callback);
this.events.set(event, callbacks);
return this;
}
We validate input types and throw errors if invalid. Returning this enables method chaining.
3. Publishing Implementation
javaScript
publish(event, ...args) {
const callbacks = this.events.get(event) || [];
callbacks.forEach(callback => callback(...args));
return this;
}
get event and run it's callback one by one.
4. Unsubscription Logic
javaScript
unsubscribe(event, callback) {
if (!this.events.has(event)) {
return this;
}
const callbacks = this.events.get(event);
const filteredCallbacks = callbacks.filter(cb => cb !== callback);
if (filteredCallbacks.length === 0) {
this.events.delete(event);
} else {
this.events.set(event, filteredCallbacks);
}
return this;
}
we will remove the event when it has no callbacks after filtered.
Single-Fire Events
javaScript
subscribeOnce(event, callback) {
const once = (...args) => {
callback(...args);
this.unsubscribe(event, once);
};
return this.subscribe(event, once);
}
We define a higher-order function that automatically unsubscribes itself after executing the callback.
Clear all Subscriptions
javaScript
clear() {
this.events.clear();
return this;
}
use Map's clear function to clean the store.
Complete Code
javaScript
class PubSub {
constructor() {
this.events = new Map(); // event -> callback[]
}
subscribe (event, callback) {
if (typeof event !== 'string') {
throw new TypeError('Event name must be a string');
}
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}
const callbacks = this.events.get(event) || [];
callbacks.push(callback);
this.events.set(event, callbacks);
return this;
}
publish(event, ...args) {
const callbacks = this.events.get(event) || [];
callbacks.forEach(callback => callback(...args));
return this;
}
unsubscribe(event, callback) {
if (!this.events.has(event)) {
return this;
}
const callbacks = this.events.get(event);
const filteredCallbacks = callbacks.filter(cb => cb !== callback);
if (filteredCallbacks.length === 0) {
this.events.delete(event);
} else {
this.events.set(event, filteredCallbacks);
}
return this;
}
subscribeOnce(event, callback) {
const once = (...args) => {
callback(...args);
this.unsubscribe(event, once);
};
return this.subscribe(event, once);
}
clear() {
this.events.clear();
return this;
}
}
