In the previous article we learnt what event-driven programming is and how you can leverage it to refactor your code. We also learnt circumstances under which you may use an event-driven approach instead of other conventional approaches. I highly recommend that you read that first.
In this article, we shall learn how event-driven systems are built from the ground up. We shall use JavaScript
for the examples but the principles still apply in other languages.
In any Event System there must be a defined set of functions:
1. Registration function
This function is usually defined by an on()
method. It registers an event listener which is an anonymous function or callback function.
An event listener is a function that defines the actions to be taken once the event is dispatched at any given point in your program.
The on()
method may also take on an optional target context
parameter that defines the context that the listener is called from.
The usual form of an event registration function is as follows:
EventEmitter.on(event, listener, target|context?)
Creating our on()
method is very straightforward. We just need to push the event and it’s listener to an array or object which we’ll use throughout the Event Emitter object.
Create a EventEmitter.js
file and add the following code:
/** * Manages all events */ export default { registeredEventListeners: [], /** * Registers an event * @param {String} event * @param {Function} handler * @param {Object} target * @returns void */ on: function(event, handler, target) { /** * Apparently I can't get the callee.caller because of webpack's strict mode */ // if (!target) { // target = arguments.callee.caller; // } // check if event already exists if (!this.registeredEventListeners[event]) { this.registeredEventListeners[event] = []; //register event this.registeredEventListeners[event].push({handler: handler, target: target}); } } }
2. De-registration function
This method is usually defined as an off() method. It basically removes the event listeners for a given event from the list of registered event listeners.
I’ve added a helper method to reduce duplication as we add more methods to the Event Emitter.
/** * Removes a listener for a given event * @param {String} event * @param {Function} handler * @returns void */ off: function(event, handler) { try{ var listeners = this.getListeners(event); if (listeners) { var index = listeners.indexOf(event); listeners.slice().map(function(listener){ if(listener.handler === handler) listeners.splice(index,1) }); } }catch(e){ throw new Error("Listener not turned off. " + e); } return this; }, /** * Helper function that gets all listeners for given event * @param {String} event * @returns Array */ getListeners: function(event) { if (!this.registeredEventListeners[event]) { this.registeredEventListeners[event] = []; } return this.registeredEventListeners[event]; }
3. Dispatching function
In order to fire/dispatch events, we have to call the listeners attached to the given event and also keep track of them.
First, add a fireEvents
array to the EventEmitter class. This array keeps track of all dispatched events.
firedEvents: [],
The following snippet shows the implementation of a dispatch function:
firedEvents: [], /** * Calls/Fires a registered event * @param {String} event */ dispatch: function(event){ try{ // get all listeners for that event var listeners = this.getListeners(event); if(!listeners || !listeners[0]) return var args = [].slice.call(arguments, 1); var that = this; listeners.slice().map(function(i){ i.handler.apply(i.target, args); that.firedEvents.push({event: event, handler: i.handler, target: i.target}); }); }catch(e){ // console.log("Event not fired. "+ e); throw new Error("Event not dispatched. "+ e); } return this; }
4. Clearing function
A clearing function just deletes both the registered and fired listeners. It effectively resets everything back to default.
Usually denoted as clear()
.
/** * clear all trackers for listeners and fired events */ clear: function(){ this.registeredEventListeners = []; this.firedEvents = []; return this; }
Conclusion
We now have a simple but effective event manager that can help you clean up your code. This tutorial basically provides the building blocks of most event emitters in NodeJs, Laravel and many more.
An Event Emitter is like a real-life event manager who ensures that the various events scheduled for the calendar are well organized with the right people (listeners/handlers) and occur at the right place (target/context).
In the next tutorial, we’ll refactor the game and show how we can use this class to refactor some of the code in the little game we built.
You can get the full code of a simple NPM package I built here. Feel free to contribute.
Also published on Medium.