Skip to Content
Course content

201: Practice Exercise: Implementing a Simple Event Emitter Class

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely spent plenty of time using addEventListener in the browser, but as you move into more complex application architecture, you'll find that you often need this "pub-sub" (publisher-subscriber) behavior in places where the DOM doesn't exist—like in a backend service or a state management layer. Implementing your own Event Emitter is a rite of passage in JavaScript because it forces you to think about how data flows between decoupled parts of your system.

The "Single Slot" Mistake

When I first started building these, I took the path of least resistance. I'd create a class and just add a property for the event I cared about. If I were building a User Auth system, I might do something like this:

class AuthEmitter {
  constructor() {
    this.onLogin = null;
  }

  on(callback) {
    this.onLogin = callback;
  }

  emit() {
    if (this.onLogin) this.onLogin();
  }
}

On the surface, this works. But it's fragile. The moment you realize that three different parts of your app need to know when a user logs in—say, the Header component to show the profile picture, the Analytics module to track the session, and the Dashboard to fetch user data—this code breaks. You've created a "single slot" system. Every time you call .on(), you're overwriting the previous listener. In a real production environment, this leads to those phantom bugs where some features just stop working because another developer added a listener elsewhere in the codebase.

Moving to a Registry of Arrays

To fix this, we have to stop thinking about a single callback and start thinking about a registry. Instead of a property, we use an object (or a Map) where the keys are the event names and the values are arrays of functions. This allows us to support any number of events, each with any number of listeners.

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  emit(eventName, data) {
    const listeners = this.events[eventName];
    if (listeners) {
      listeners.forEach(callback => callback(data));
    }
  }
}

Now we have a flexible system. I can call emitter.on('userLogin', handleUI) and emitter.on('userLogin', handleLogs) without them fighting each other. I've also added a data argument to the emit method, which is essential. An event that just says "something happened" is rarely useful; you usually need to know what happened or who it happened to.

The Memory Leak Problem

Here is where most developers stop, but this is where the "professional" version differs from the "tutorial" version. If you keep adding listeners to a long-lived object but never remove them, you're creating memory leaks. In a single-page app, if a component mounts, adds a listener, and then unmounts without removing that listener, that component stays in memory forever because the EventEmitter still holds a reference to its method.

To solve this, we need an off method. The trick here is that you can't just clear the array; you have to remove the specific function reference. I usually use filter for this because it's clean and immutable, though splice is slightly more performant if you're dealing with thousands of listeners (which, let's be honest, you probably aren't).

off(eventName, callbackToRemove) {
  if (!this.events[eventName]) return;
  
  this.events[eventName] = this.events[eventName].filter(
    callback => callback !== callbackToRemove
  );
}

Keep in mind: for off to work, you can't use anonymous arrow functions in your .on() call. If you pass () => console.log('hi'), you're creating a new function reference every time, and filter will never find a match to remove. You have to name your functions if you want to be able to unsubscribe from them later.




📋 Practical Task

Exercise: Build a Shopping Cart Notification Hub

Your goal is to implement a fully functional CartEmitter class that manages events for an e-commerce shopping cart. This hub will allow different parts of the application (the Cart Icon, the Total Price calculator, and the Inventory system) to stay in sync without being directly connected.

Requirements:

  • Implement an on(event, callback) method to register listeners.
  • Implement an emit(event, data) method that triggers all registered callbacks for that event, passing the data along.
  • Implement an off(event, callback) method to remove a specific listener.

Test your implementation with the following scenario:

  1. Create an instance of CartEmitter.
  2. Create two separate functions: updateCartCount and logTransaction.
  3. Subscribe both functions to the 'itemAdded' event.
  4. Emit the 'itemAdded' event with a product object (e.g., { name: 'Mechanical Keyboard', price: 150 }) and verify both functions execute.
  5. Unsubscribe logTransaction using the off method.
  6. Emit 'itemAdded' again and verify that only updateCartCount executes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.