Design Patterns in JavaScript: Publish-Subscribe Pattern

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.