DEV Community

Cover image for Today's rabbit hole: what is Event Driven Programming and how to code your own EventEmitter
Pere Sola
Pere Sola

Posted on

Today's rabbit hole: what is Event Driven Programming and how to code your own EventEmitter

Note: this is first and foremost the tale of a journey. It acts as a how-to, but I also want to share my thought process and how I learnt along the way. If any of the below is total nonsense, let me know in the comments!

Trigger: few months ago I was put in a situation where I was asked to build my own EventEmitter. I didn't have a clue and it was very embarrassing. The tale below is my quest to learn about it.

I explained it to a friend and he told me: ah, you are supposed to build an [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/EventTarget)!. Ehhhh, what the hell do you mean?!

I Googled what is js eventemitter and landed in a nodejs tutorial.

Reading the first few lines of the article made me think about the Javascript Event Loop, which I have read quite a lot about it.

What is Javascript's Event Loop?

At Lambda School we have a Slack channel where students can share questions they may have been asked in recent job interviews. In my growth mindset attitude and my quest to learn what really matters in the industry, I started to track these questions and read about the topics. Believe or not, the first question I tracked was What is Javascripts Event Loop, and how does it work?. I had done a bit of research and had settled with the following 2 articles:

  1. Flavio Copes' The JavaScript Event Loop
  2. Sukhjinder Arora's Understanding Asynchronous JavaScript

The main idea is that Javascript is single threaded. That means that things run one after another and anything that takes time to return blocks the execution of the code. As Flavio ilustrates very well, the event loop continuously checks the call stack which, like any Stack, it's Last In First Out (LIFO). When it finds a function to execute, it adds it to the Stack

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()
Enter fullscreen mode Exit fullscreen mode

^ Source: Flavio Copes article

Alt Text

^ Source: Flavio Copes article

What happens when there is async code. Flavio adds a setTimeout() in his code:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()
Enter fullscreen mode Exit fullscreen mode

^ Source: Flavio Copes article

Alt Text

^ Source: Flavio Copes article

In this case, setTimeOut(), even if triggered after 0 miliseconds, is async. Browser or Node.js start a timer and, when the timer expires, the code that it needs to execute is added to something called the Message Queue - which sits at the bottom of the Stack. I learnt that user triggered events like mouse clicks are also added to that queue.

ES6 introduced the Job Queue for Promises. That means that the result of a Promise (i.e. fetching data from an API) is executed as soon as possible rather than being added to the Message Queue.

As I read in here, whenever a task gets completed in the Stack, Node fires an event that signals the event-listener to execute. Event handlings is based on the observer pattern. The observer pattern is a a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. (source: Wikipedia). The article even has code snippets in JS using... RxJS. Oh dear, I have heard about RxJS quite a lot. Putting RxJS in the parking lot for now

Ok man, what about the EventEmitter I was supposed to code?

I went back to the first resource I mentioned. There I read that Many objects in a Node emit events, for example, a net.Server emits an event each time a peer connects to it, an fs.readStream emits an event when the file is opened. All objects which emit events are the instances of events.EventEmitter.. And it goes on to explain that EventEmitter is a Class that lives inside the event module.

I went straight to the example listed in the article to understand the methods. And to my surprise, it all made sense! I coded a little example and I was happy it all made sense. I am not building my own eventEmitter at this point, just practising the methods. The only doubt I had is how to pass arguments to the listeners. For instance:

  • I wanted to add the name of the event to the sentence being logged:
const listener1 = (argument) => {
    console.log(`Hey, I am listener 1 on event ${argument}`);
};
Enter fullscreen mode Exit fullscreen mode

I thought I would just invoke the listener with an argument in it:

emitter.addListener('test', listener1('test');
Enter fullscreen mode Exit fullscreen mode

That threw an error. I Googled it and found the answer here: it turns out that when adding listener you are only declaring the name of the function, not invoking it. The arguments are passed when the event is emitted. Like so:

emitter.addListener('test', listener1);
emitter.emit('test', 'arg1');
Enter fullscreen mode Exit fullscreen mode

What if we have several listeners that are expecting different arguments? Like so:

const listener1 = (arg1) => {
    console.log(`Hey, I am listener 1 on event ${arg1}`);
};

const listener2 = (arg2) => {
    console.log(`Hey, I am listener 2 on event ${arg2}`);
    return 'listener 2';
};
Enter fullscreen mode Exit fullscreen mode

My understanding from the Stack Overflow answer above is that all arguments possibily expected by any of the listeners have to be passed, and all possible arguments need to be declared in the listener functions. Like so:

const listener1 = (arg1, arg2) => {
    console.log(`Hey, I am listener 1 on event ${arg1}`);
};

const listener2 = (arg1, arg2) => {
    console.log(`Hey, I am listener 2 on event ${arg2}`);
    return 'listener 2';
};
Enter fullscreen mode Exit fullscreen mode

I actually don't need arg2 in listener1, because it comes after arg1, but I definitely need it in in listener2 otherwise arg2 will be the first argument being passed in the emit method. Then I am emitting the event test like so:

emitter.emit('test', 'arg1', 'arg2');
Enter fullscreen mode Exit fullscreen mode

It is actually explained in here but I saw it only later!

Ok. This is what comes out of the the Node.js box. How do you build your own EventEmitter?

This is the destination of my rabbit hole afterall! I Googled it and found the following tutorials:

  1. How to Create Your Own Event Emitter in JavaScript by Oleh Zaporozhets
  2. How to code your own event emitter in Node.js: a step-by-step guide by Rajesh Pillai

I had fun reading and finally learning to implement my eventEmitter. The key concept that I had to grasp was the following:

  • An emitter emits one or many events.
  • An event triggers one or many listeners. A listener is a call back function: a function that executes when the event is received. But first, you need to add or register (I think people also call is subscribe) the listener to the event.

So, conceptually, it makes sense to store the events in an object inside the emitter. It also makes sense to store every listener to an event inside an array. This way, when an event is emitted, we look up for the event inside the object (it is O(1)) and we then execute all the listeners stored in it in sequence (it is O(n)). I suppose that, since all listeners need to be executed, there is no way to improve O(n).

I personally always have a lot of fun working with Classes - I find OOP programming to be super logical and I have fun because everything in it is linked to each other. I know, JS is not pure OOP because it is prototype based... let's leave this for another day.

Now, how do we build the EventEmitter Class?

I found Rajesh's article great because it builds a lot of the native Node.js eventEmitter methods (i.e. listenerCount(), rawListeners() etc.).

Class

We first build the Class constructor:

class EventEmitter {
    constructor() {
        this.events = {};
    }
}
Enter fullscreen mode Exit fullscreen mode

As discussed, the events property will be an object and we will access the event listeners with this.events[name].

Add Listener

Next, we create the addListener method. It takes two arguments: name of the event and listener (function to be executed when event is emitted):

addListener(name, listener) {
// if event name has not yet been recorded in the object (it is not a property of `this.events` yet), we do it and initialise an array
    if (!this.events[name]) {
        this.events[name] = [];
    }
// we push the `listener` (function) into the array
    this.events[name].push(listener);
}
Enter fullscreen mode Exit fullscreen mode

On

since .on and addListener are the same, we code .on like that:

on(name, listener) {
    return this.addListener(name, listener);
}
Enter fullscreen mode Exit fullscreen mode

Remove Listener

Next, we can code removeListener(), which is removing the listener from the array in this.events[name]:

removeListener(name, listenerToRemove) {
// if event name does not exist in `this.events` object, we throw an error because nothing can be removed
    if (!this.events[name]) {
        throw new Error(`Can't remove listener, event ${name} doesn't exist`);
    }
// we use one of the high order methods (filter) to filter out the listener to be removed from the array
    this.events[name] = this.events[name].filter((listener) => {
        return listener != listenerToRemove;
    });
}
Enter fullscreen mode Exit fullscreen mode

Off

Similar to .on, .off is equivalent to removeListener(). So:

off(name, listenerToRemove) {
    return this.removeListener(name, listenerToRemove);
}
Enter fullscreen mode Exit fullscreen mode

Once

Next, I learnt a lot reading how Rajeh implemented the .once method. once means that the listener will be automatically removed after it has executed once. So:

once(name, listener) {
// we check if event exists in the object, and if not we create an intialise an array
    if (!this.events[name]) {
        this.events[name] = [];
    }
// we create a wrapper function, which is the one that will be added to the array. This wrapper function executes the listener that we want to add and calls .removeListener
    const onceWrapper = (...arg) => {
        listener(...arg);
        this.removeListener(name, onceWrapper);
    };
// we push the wrapper function into the array
    this.events[name].push(onceWrapper);
}
Enter fullscreen mode Exit fullscreen mode

The thing that tripped me here is that I was initially removing the listener that I wanted to add. No, I should remove the wrapper because (remember we use the method filter to remove listeners?) otherwise we won't find it and nothing will be removed. It took me a while to find out what I was doing wrong.

Emit

Next, we code emit. Emit has an obligatory argument (the name of the event) and then you can pass as many arguments you want to the listeners. That is why I used ...arg above, since we don't know how many arguments will be passed ahead of time. Maybe a certain listener expects 3 (this number is just an example), and all listeners recorded for the event (added to the array) need to be prepared to receive that many arguments in case their arguments come after these 3. Unless I am wrong, you can achieve this by spreading the arguments (...args):

emit(name, ...data) {
    if (!this.events[name]) {
        throw new Error(`Can't emit an event. Event ${name} does not exist.`);
    }

    this.events[name].forEach((cb) => {
        cb(...data);
    });
}
Enter fullscreen mode Exit fullscreen mode

First, if event does not exist (no property with the name of the event is found inside the this.events object), then we throw an error. If we find the event, we iterate over the array with forEach and we execute the listener passing the arguments, if any.

I have seen implementations out there that seem to have forgotten the arguments, or maybe I am missing something. In any case, mine seems to work, if you spot any mistake please let me know in the comments.

Listener count

Next, listenerCount. It takes one argument (the name of the event) and returns the count of listeners (the ones stored in the array). I think the code is self explanatory:

listenerCount(name) {
    if (!this.events[name]) {
        this.events[name] = [];
    }
    return this.events[name].length;
}
Enter fullscreen mode Exit fullscreen mode

Raw Listeners

The last one I coded is rawListeners, which return an array of the listeners that have been registered to an event. While this had the most misteriours name to me, it's the easiest of all afterall - it just needs to return the array.

rawListeners(name) {
    return this.listeners[name];
}
Enter fullscreen mode Exit fullscreen mode

And that is it: you can now instantiate your new EventEmitter class and run the methods on this instance:

const myEmitter = new EventEmitter();
myEmitter.on('testEvent', handler1);
myEmitter.on('testEvent2', handler1);
myEmitter.emit('testEvent', 'hey there');
myEmitter.emit('testEvent', 'firing event again');
myEmitter.emit('testEvent', 'and again');

etc.
Enter fullscreen mode Exit fullscreen mode

Hope you enjoyed it! If you spot any mistakes, let me know in the comments.

Latest comments (0)