Jochem Stoel
Posted on December 8, 2018
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!
Posted on December 8, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.