Lessons From Making My Own EventEmitter
Tags: javascript, techinterviews
Published: 2019-12-09 21:01:21

I recently completed a [pramp](https://dev.to/pramp) practice problem that I found very enjoyable. It involved creating your own event emitter class in Javascript with the methods `on`, `off`, `emit`, and `once`.

`on` subscribes a call back to an event name.

`off` removes a callback from an event name.

`emit` calls all the callbacks associated with an event name and any arguments passed to emit.

`once` is similar to `on`, with the added logic of unsubscribing itself after being called once.

## Creating the class and Subscribing

Let's start off by making the class and implementing the `on` method.

```js
class EventEmitter {
  constructor(){
    this.events = {}
  }

  on(name, cb) {
    if (!this.events[name]) {
      this.events[name] = []
    }

    this.events[name].push(cb)
  }
}

```

Upon instantiation of an `EventEmmitter` class, an internal state housing all the event names is created. The `on` method takes a `name` string and `cb` function. The method will then add the `cb` to an array keyed to the event name. If no previous callbacks were added to this event name, a new key is created.

An example of this method in action:

```js
const emitter = new EventEmitter()

emitter.on('click', () => console.log('I got clicked'))
```

## Emitting and Unsubscribing

Now let's extend the EventEmitter class with some more functionality.

The actual emitting of an event can be done in a for loop, iterating through the `cb`'s stored to an event name. In this example, I am using the ES6 spread (`...`) to store all the arguments passed to the `emit` and passed them to the callbacks within the loop.

```js

// within EventEmitter class

  emit(name, ...args) {
    if (!this.events[name]) {
      // Ignore event names we don't have callbacks for.
      return;
    }

    for (let cb of this.events[name]) {
      cb(...args);
    }
  }

// usage
  emitter.emit("click")
```

Next let's unsubscribe a callback from an event name. Using this simple implementation, the only way to unsubscribe an event is by keeping a reference to the callback you made. We will need it for comparing the callbacks within the callback array. Later on in the blog post I'll talk about another method of unsubscribing.

```js
// within EventEmitter class

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

// usage
  const logClicks = () => console.log('I got clicked')
  emitter.on('click', logClicks)
  emitter.emit('click') // I got clicked!
  emitter.off('click, logClicks)
  emitter.emit('click') // Nothing happens.
```

## Interesting Part

The final method, `once`, is where things get interesting. The imperative approach could be keeping a some additional internal state for `once` callbacks, and performing a check every time we run emit to see if the callback exists in the once state.

There is a much more elegant way of removing the `once` callback by leveraging javascript's first class treatment of functions.

Instead of storing more state, I can wrap the passed in callback with  another function, and add some additional logic to it to remove itself after it gets called. This is what it would look like:

```js
  once(name, cb) {

    this.on(name, function onceCB(...args) {
      cb(...args)
      this.off(name, onceCB)
    })
  }
```

Trying to run this code alone will not work though. `this` inside of `onceCB` is `undefined`! What do we do???

## Context in Javascript

Context in javascript is a confusing topic that trips people up all the time. This is where some lesser known javascript APIs and arrow functions come in. Objects in Javascript have 3 methods that can be used for defining a `this` context. They include `bind`, `call`, and `apply`.

`bind` may be familiar with those who have some React experience. You would typically see a bind for methods that get passed as event handler functions. These methods need a bind to the component class they belong to because without it, the function would automatically bind to its nearest context where it gets called. In our case above, the function is being called in the global scope which is undefined.

`call` and `apply` are similar with a small difference. Both are used for invoking a function. Both take a context as its first parameter. `call` takes arguments individually, while `apply` takes an array of arguments. Either can be used interchangeably depending on your coding style or the coding styles defined by your project.

```js
  someFunc(a, b)
  
  someFunc.call(this, a, b)
  
  someFunc.apply(this, [a,b])

```

Arrow functions, introduced in ES2015 (ES6) do a bit of magic behind the scenes, and automatically bind functions to the context where they are defined. This simplifies functions for developers as you usually want your functions to use the context where they were defined, reducing the overhead of remembering to bind.

Now that we know a bit more about how context works in javascript, let's look at some ways we can fix the `once` method above:

Using call or apply:
```diff
  // No need to modify the `once` method.

  emit(name, ...args) {
    if (!this.events[name]) {
      // Ignore event names we don't have callbacks for.
      return;
    }

    for (let cb of this.events[name]) {
-      cb(...args);
+      cb.apply(this, args); // or cb.call(this, ...args)
    }
  }

```

Using arrow functions:
```diff

  // No need to modify the `emit` method

  once (name, cb) {
-   this.on(name, function onceCB(...args) {
-     cb(...args)
-     this.off(name, onceCB)
-   })
+   const wrappedCB = (...args) => {
+     this.off(name, wrappedCB);
+     cb(...args);
+   };
+   this.on(name, wrappedCB);
  }
```

I tried to use `bind` in a similar fashion as the arrow function method but I was still getting the `TypeError: Cannot read property 'off' of undefined` error. I was able to get the once method to work without having to use apply or call in emit by storing a reference to this and using it in side of the `wrappedCB`

```diff
  once (name, cb) {
+   const self = this;
    this.on(name, function singleCB(...args) {
-     this.off(name, singleCB);
+     self.off(name, singleCB);
      cb(...args);
    });
```

## Bonus round, a nicer Unsubscribe API

Having to store your callback for the sole purpose of unsubscribing is not the nicest API. You may prefer to just write the callback inline with the `on` call. The pattern I am about to show you is used in popular libraries like the Firebase Web client and jsdom to handle unsubscribing or cleaning up an instance.

Inside of the `on` method. instead of returning nothing, it can return a function which can call the off method for us.

```js
// At the end of `on`

// using self
   const self = this;
   function cleanup() {
     self.off(name, cb);
   }
   return cleanup;

// or using arrow
   return () => {
     this.off(name, cb);
   };

// usage

const jelly = emitter.on('jelly', function(...args) console.log('jelly time', ...args))
emitter.emit('jelly', '1', '2', '3') // jelly 1 2 3
jelly() // unsubscribe the subscription
emitter.emit('jelly', '1', '2', '3') // nothing happens
```

## Summary

Creating your own event emitter was a fun exercise. I got to practice the  subscriber pattern in javascript which is typically abstracted away from me.

I got to see the motivation behind arrow functions and how they greatly simplify writing javascript applications. 

Lastly, I got to use the `apply` and `call` methods for the first time! I typically focus on writing application logic so this change of scenery gave some great insight into what more advanced javascript looks like and helped me gain a better grasp of how `this` works. 

If you made it this far I hope you have learned something new today and try this out on your own. 

Until next time... 

Here is the final working class 

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

  on(name, cb) {
    if (!this.events[name]) {
      this.events[name] = [];
    }

    this.events[name].push(cb);

    // using self
    // const self = this;
    // function cleanup() {
    //   self.off(name, cb);
    // }
    // return cleanup;

    // using arrow
    return () => {
      this.off(name, cb);
    };
  }

  once(name, cb) {
    // Using arrow:
    const wrappedCB = (...args) => {
      this.off(name, wrappedCB);
      cb(...args);
    };
    this.on(name, wrappedCB);

    // Using self object:
    // const self = this;
    // this.on(name, function wrappedCB(...args) {
    //   self.off(name, wrappedCB);
    //   cb(...args);
    // });

    // Original
    // this.on(name, function singleCB(...args) {
    //   this.off(name, singleCB);
    //   cb(...args);
    // });
  }

  emit(name, ...args) {
    if (!this.events[name]) {
      return;
    }

    for (let cb of this.events[name]) {
      cb(...args);
      // If not using arrow or self inside of `once`
      // cb.apply(this, args);
      // cb.call(this, ...args);
    }
  }

  off(name, cb) {
    if (!this.events[name]) {
      return;
    }

    this.events[name] = this.events[name].filter(callback => callback !== cb);
  }
}
```