DEV Community

Cover image for Write your own chainable Event Emitter class for Node and Browser
Jochem Stoel
Jochem Stoel

Posted on

Write your own chainable Event Emitter class for Node and Browser

It is saturday morning, I am still waiting for coffee so let's do something trivial to warm up for the day.

Write your own chainable Event Emitter class for Node and Browser

An event emitter in JavaScript generally has three methods.

addEventListener

Add / register a listener that will be called when an event is dispatched.

removeEventListener

Remove / unregister an event listener.

dispatchEvent

This method is used to trigger an event of a certain type.

The class

First of all we make sure Emitter is instanciated and not called as a function.

function Emitter() {
    if (!(this instanceof Emitter)) throw new TypeError('Emitter is not a function.')
    // ...
}

Declare a private variable to store listeners. This array is going to be populated with more arrays where array[0] is the event type and array[1] the callback function.

/**
 * Store event handlers here.
 * @type {Array}
 * @private
 */
let handlers = []

addEventListener

This method will add/register a new event listener for events of the provided type by adding an item of type array to handlers where array[0] is the type and array[1] the callback.

/**
 * Add event listener.
 * @param {string} event type 
 * @param {function} callback function
 */
this.addEventListener = (type, fn) => {
    handlers.push([type, fn])
}

Once instanciated, you call addEventListener as follows:

emitter.addEventListener('message', message => console.log('received a message!', message))

removeEventListener

We also need to be able to remove event listeners we no longer need. To do this, we need to remove all items in handlers where item[0] is the event type.

/**
 * Remove event listener.
 * @param {string} event type 
 * @param {function} callback function
 */
this.removeEventListener = (type, fn = true) => {
    handlers = handlers.filter(handler => !(handler[0] == type && (fn == true ? true : handler[1] == fn)))
}
emitter.addEventListener('ready', console.log) // console.log will be called when a ready event happens
emitter.removeEventListener('ready', console.log)  // console.log will no longer be called on ready events

dispatchEvent

The method to trigger an event is called dispatchEvent in the browser. In Node environments it is generally called emit.
We are going to slightly modify this function so that it supports wildcard event types (as seen in https://www.npmjs.com/package/eventemitter2). Additionally besides the event data, a a second argument type is given to the event handler. When you implement wildcard event type support, this argument is useful to determine what's what.

// without the type argument, this event could be anything
emitter.addEventListener('*', (event, type) => console.log(`an event of type = ${type} was emitted.`))
emitter.addEventListener('user:*', (event, type) => console.log(`something usery happened.`))
/**
 * Dispatch event.
 * @param {string} event type 
 * @param {any} event data
 */
 this.dispatchEvent = (type, data) => {
    handlers.filter(handler => new RegExp("^" + handler[0].split("*").join(".*") + "$").test(type)).forEach(handler => handler[1](data, type))
 }

getEventListeners

Perhaps you want to be able to get/list all event listeners (of a certain type).

/**
 * Get list of event handlers (of a type) or all if type is not specified
 * @param {string} [event type] (optional) 
 */
 this.getEventListeners = type => {
    if (!type)
        return handlers
    let fns = []
    handlers.filter(handler => handler[0] == type).forEach(handler => fns.push(handler[1]))

    return fns
}

clearEventListeners

Let's also add this extra method that will clear all event listeners by reinitializing handlers.

/**
 * Clear event listeners
 * @param {string} [event type] (optional)
 */
    this.clearEventListeners = () => { handlers = [] }

So far

Our Emitter class now looks something like this.

function Emitter() {
    if (!(this instanceof Emitter)) throw new TypeError('Emitter is not a function.')

    let handlers = []

    this.addEventListener = (type, fn) => {
        handlers.push([type, fn])
    }

    this.removeEventListener = (type, fn = true) => {
        handlers = handlers.filter(handler => !(handler[0] == type && (fn == true ? true : handler[1] == fn)))
    }

    this.dispatchEvent = (type, data) => {
        handlers.filter(handler => new RegExp("^" + handler[0].split("*").join(".*") + "$").test(type)).forEach(handler => handler[1](data, type))
    }

    this.clearEventListeners = () => { handlers = [] }

    this.getEventListeners = type => {
        if (!type)
            return handlers

        let fns = []
        handlers.filter(handler => handler[0] == type).forEach(handler => fns.push(handler[1]))

        return fns
    }
}

Congratulations! You have a working event emitter class. Try it yourself:

var emitter = new Emitter()
emitter.addEventListener('ready', console.log)
emitter.addEventListener('foo.*', (event, type) => console.log({type,event}))
emitter.dispatchEvent('ready', Date.now())
emitter.dispatchEvent('foo.bar', 'blabalbla')
emitter.removeEventListener('ready', console.log)
emitter.clearEventListeners()

But we are not done, I promised a chainable event emitter. Chainable means that the Emitter is a singleton that always returns itself, allowing you to keep calling methods on it.

Shortcuts

Because we don't like to write addEventListener and dispatchEvent all the time, let's add these shortcuts. These shortcuts all return this at the end to make chains.

 /**
     * Shortcut for addEventListener.
     * @param {string} event type 
     * @param {function} callback function 
     */
    this.on = (type, fn) => {
        this.addEventListener(type, fn)
        return this /* chain */
    }

    /**
     * Shortcut for removeEventListener
     * @param {string} event type 
     * @param {function} callback function 
     */
    this.off = (type, fn) => {
        this.removeEventListener(type, fn)
        return this /* chain */
    }

    /**
     * Shortcut for dispatchEvent
     * @param {string} event type 
     * @param {any} event data
     */
    this.emit = (type, data) => {
        this.dispatchEvent(type, data)
        return this /* chain */
    }

     /**
     * Shortcut for clearEventListeners
     * @param {string} event type 
     */
    this.clear = type => {
        this.clearEventListeners(type)
        return this
    }

    /**
     * 
     * @param {string} [type]
     */
    this.list = type => this.getEventListeners(type)

Now our Event Emitter class can be accessed like:

emitter.on('message', message => console.log(message).on('open', onOpen).on('error', console.error).emit('ready', { status: 200, details: 'this is a ready event'})

Final result: class Emitter

Your final Emitter class should look something like this:

/**
 * Simpler EventTarget class without the need to dispatch Event instances.
 * @constructor
 * @returns {Emitter} new instance of Emitter
 */
function Emitter() {
    if (!(this instanceof Emitter)) throw new TypeError('Emitter is not a function.')

    /**
     * Store event handlers here.
     * @type {Array}
     * @private
     */
    let handlers = []

    /**
     * Add event listener.
     * @param {string} event type 
     * @param {function} callback function
     */
    this.addEventListener = (type, fn) => {
        handlers.push([type, fn])
    }

    /**
     * Remove event listener.
     * @param {string} event type 
     * @param {function} callback function
     */
    this.removeEventListener = (type, fn = true) => {
        handlers = handlers.filter(handler => !(handler[0] == type && (fn == true ? true : handler[1] == fn)))
    }

    /**
     * Dispatch event.
     * @param {string} event type 
     * @param {any} event data
     */
    this.dispatchEvent = (type, data) => {
        handlers.filter(handler => new RegExp("^" + handler[0].split("*").join(".*") + "$").test(type)).forEach(handler => handler[1](data, type))
    }

    /**
     * Clear event listeners
     * @param {string} [event type] (optional)
     */
    this.clearEventListeners = () => { handlers = [] }

    /**
     * Get list of event handlers (of a type) or all if type is not specified
     * @param {string} [event type] (optional) 
     */
    this.getEventListeners = type => {
        if (!type)
            return handlers

        let fns = []
        handlers.filter(handler => handler[0] == type).forEach(handler => fns.push(handler[1]))

        return fns
    }

    /**
     * Shortcut for addEventListener.
     * @param {string} event type 
     * @param {function} callback function 
     */
    this.on = (type, fn) => {
        this.addEventListener(type, fn)
        return this /* chain */
    }

    /**
     * Shortcut for removeEventListener
     * @param {string} event type 
     * @param {function} callback function 
     */
    this.off = (type, fn) => {
        this.removeEventListener(type, fn)
        return this /* chain */
    }

    /**
     * Shortcut for dispatchEvent
     * @param {string} event type 
     * @param {any} event data
     */
    this.emit = (type, data) => {
        this.dispatchEvent(type, data)
        return this /* chain */
    }

     /**
     * Shortcut for clearEventListeners
     * @param {string} event type 
     */
    this.clear = type => {
        this.clearEventListeners(type)
        return this
    }

    /**
     * 
     * @param {string} [type]
     */
    this.list = type => this.getEventListeners(type)
}

module.exports = Emitter

Done!

Good job, you have successfullly copy pasted my code!

Top comments (1)

Collapse
 
justintime4tea profile image
Justin Gross • Edited

Anyone working with event emitters that support wild cards here's a friendly heads up. Don't listen to wild card and in the callback emit any events without some matching on the event... :) Callstack could explode. Be sure to factor in a matcher of sorts which ignores your own emitted messages if emitting from within the wild card callback!