How to write a custom event system in JavaScript

When building complex applications it's always a good idea to take a step back and think through architectural choices from the get-go. One pattern that really forces us to do that is an event system or the closely related PubSub pattern, which we'll take a closer look at in this article.

The biggest advantage of this pattern is that we get to decouple parts of our application and have them talk to each other through a middleman, or in simpler cases, we just broadcast an event and have a listener that executes when the event is triggered. A big advantage of this pattern is that all our events get routed through the same logic before being picked up by the listeners, which allows us to do some extra work and extend all events in our system later on.

Let's clear up some terms before we move into code. Given a simple example like on-page notifications, our core functionality that will be executed can be a simple alert() . Now to not call it directly but actually use an event system, we need to implement an Observer that will listen to incoming events, delegate our invocation and call all the right functions for us.

const Observer = function() {
    var self = this
    self.events = {}

    return {

        dispatch: function(eventName) {
            // We already know we want to be able to call our observer and
            // it will need to dispatch the actual event
        },

        on: function(eventName, callback) {
            // Our observer will need to be able to listen itself for incoming events
            // that we'll notify the listeners about (via callbacks)
        }

    };

}();

This Observer pattern above will be our middleman and listen to events when we define them in like this: Observer.on('eventName', callback). At the same time, it allows us to trigger those events via Observer.dispatch('eventName') whenever we want the receivers to be notified. The most important part inside our observer is the events object that will hold all callbacks in the form of {[eventName: callback()]}. Let's go ahead and fill the functions with code to see how the Observer pattern works in action.

Implementing the observer pattern

const Observer = function() {
    var self = this
    self.events = {}

    return {

        dispatch: function(eventName) {
            var events = self.events[eventName]
            events.forEach((callback) => callback())
        },

        on: function(eventName, callback) {
            if (typeof self.events[eventName] === 'undefined') {
                self.events[eventName] = []
            }

            self.events[eventName].push(callback)
        }

    }
}()

The on method is used to register our events and we need to call it before we can actually dispatch an event. it takes an event name and a callback as parameters and pushes both into our events object as described above.

Our dispatch function that will be triggered to fire off an event, takes an event name as the only argument. In fact, this should be the same event name that we previously added to the events object when we registered the event with our on function.

And with that, we have a working event system that can be used like this:

// Listen to an event
Observer.on('testevent', function() {
    console.log('This will be executed when our event fires')
})

// Fire our event
Observer.dispatch('testevent')

Keep in mind that this is a very minimal custom event system and there are more mature systems out there. If we were to expand this minimal example, we'd need to take proper care of error handling, unbinding events (off function to our on), and implement some general checks around passed arguments.

If you want to dive deeper into the observer pattern or Pub Sub, here's a great resource for further reading with implementation details: https://addyosmani.com/resources/essentialjsdesignpatterns/book/#mediatorpatternjavascript

JavaScript
design patterns