Design Patterns in JavaScript: Publish-Subscribe Pattern
The Publish-Subscribe Pattern is a behavioral design pattern that defines a one-to-many dependency relationship, allowing multiple subscriber objects to listen to a single topic object simultaneously. When the state of the topic object changes, it notifies all subscriber objects, enabling them to update automatically.
1. Core Concepts Explained
- Publisher: The object responsible for publishing notifications when its state changes.
- Subscriber: An object interested in a specific topic and registered to listen for updates.
- Event Channel: Middleware that manages subscription relationships and passes messages.
2. Basic Implementation Steps
Step 1: Create an Event Center (Dispatcher)
class EventEmitter {
constructor() {
this.events = {}; // Stores event types and their corresponding callback function arrays
}
}
Step 2: Implement the Subscribe Method (on)
class EventEmitter {
// ... Constructor
on(eventName, callback) {
// If the event doesn't exist, create a new array
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// Add the callback function to the array for the corresponding event
this.events[eventName].push(callback);
}
}
Step 3: Implement the Publish Method (emit)
class EventEmitter {
// ... Previous code
emit(eventName, ...args) {
// Get all callback functions for this event
const callbacks = this.events[eventName];
if (callbacks) {
// Execute all callback functions in sequence
callbacks.forEach(callback => {
callback.apply(null, args);
});
}
}
}
Step 4: Implement the Unsubscribe Method (off)
class EventEmitter {
// ... Previous code
off(eventName, callback) {
const callbacks = this.events[eventName];
if (callbacks) {
// Filter out the callback function to be removed
this.events[eventName] = callbacks.filter(cb => cb !== callback);
}
}
}
3. Complete Basic Implementation
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, ...args) {
const callbacks = this.events[eventName];
if (callbacks) {
callbacks.forEach(callback => callback(...args));
}
}
off(eventName, callback) {
const callbacks = this.events[eventName];
if (callbacks) {
this.events[eventName] = callbacks.filter(cb => cb !== callback);
}
}
once(eventName, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
}
4. Usage Example
const eventBus = new EventEmitter();
// Subscribe to an event
eventBus.on('message', (data) => {
console.log('Subscriber 1 received message:', data);
});
eventBus.on('message', (data) => {
console.log('Subscriber 2 received message:', data);
});
// Publish an event
eventBus.emit('message', 'Hello World!');
// Output:
// Subscriber 1 received message: Hello World!
// Subscriber 2 received message: Hello World!
// One-time subscription
eventBus.once('one-time', () => {
console.log('This will only execute once');
});
eventBus.emit('one-time'); // Output: This will only execute once
eventBus.emit('one-time'); // No output
5. Advanced Feature Extensions
Support for namespaces:
class AdvancedEventEmitter extends EventEmitter {
on(namespacedEvent, callback) {
const [eventName, namespace] = namespacedEvent.split('.');
super.on(eventName, callback);
}
emit(namespacedEvent, ...args) {
const [eventName] = namespacedEvent.split('.');
super.emit(eventName, ...args);
}
}
6. Practical Application Scenarios
- DOM Event System: addEventListener/removeEventListener
- Vue.js EventBus: Component communication
- Node.js EventEmitter Module
- Redux State Management
- WebSocket Message Push
7. Pattern Advantages
- Decoupling: Publishers and subscribers don't need to know about each other's existence.
- Scalability: New subscribers can be easily added.
- Flexibility: Supports one-to-many communication relationships.
This pattern is widely used in front-end development, playing an important role in scenarios such as component communication and state management.