Custom Event Listeners and Dispatchers in JavaScript

If you have never built your own event model, you’re really missing out. There are lots of solutions already out there, but I highly recommend understanding the core fundamental of listening for, and dispatching code execution from, events.

In this tutorial I will explain how to think about communication between different Classes.

Lets assume I’m being delivered a package. The delivery man’s ultimate goal is to have me sign his pad, and take ownership of the package. There are steps in between though. He must get my attention when he is at the door. I’m listening for the doorbell to ring. Whether or not I’m currently asking my self the question, “is the door bell rinnging?” (or not) I am aware that when the doorbell rings, my job it to go answer the door. I’m sure you understand the concept.

In code this is made possible by linking a function to a string.

var thingsToDo = {'doorbell': function(){ alert("I'm coming");}};
thingsToDo['doorbell']();

Look at how I’m calling the function. I’m referencing the property of the thingsToDo object with a String and then promptly calling it by doing the open and close parenthesis thing. Very handy thing to know.

Lets look at how we can use this idea to tie two things together

function Doorbell() {
    var events = {};

    function callEvent(the_event) {
        if (events.hasOwnProperty(the_event)) {
            events[the_event]();
        }
    }
    this.ring = function () {
        callEvent('buzz');
    };
    this.listenFor = function (the_event, the_action) {
        events[the_event] = the_action;
    };
}

function Consciousness (in_mind) {
    in_mind.listenFor('buzz', answerTheDoor);

    function answerTheDoor() {
        console.log('Coming!');
    }
}

var buzzer = new Doorbell();
var consciousness = new Consciousness (buzzer);

buzzer.ring();

Just bask in its structure for a little bit. Doorbell is a class that has 2 methods associated with it: ring and listenFor. Consciousness listens for activity on the thing in_mind. In this way, we’re putting the function answerTheDoor up inside of Doorbell‘s events object. That way, when the doorbell is rung. We can execute that function on behalf of the listener. Look at it.

    //copied from above
    function callEvent(the_event) {
        if (events.hasOwnProperty(the_event)) {
            events[the_event]();
        }
    }
    this.ring = function () {
        callEvent('buzz');
    };
    this.listenFor = function (the_event, the_action) {
        events[the_event] = the_action;
    };

And just like that. we have a separate object passing a todo over to an object that does it. Pretty sweet. Now allow me to crush your dreams. This is not a fool-proof concept. If you listen with an object that you delete, and then you try to call it, what happens? Well, it breaks. If you have a big application full of creating listeners and then quickly deleting them and creating new ones, doesn’t that use up a lot of memory? Yup. It does… But you’re getting ahead of yourself. Focus on the concept. Its a way of handling the what happens when x happens to y relationship between Classes. Memory management just requires that you keep track of all the memory you assign. The concepts for that is simple. Clean your room. Since we have quite a bit of memory available for testing, I see no reason why I should talk about memory management yet. Instead lets beef up our listeners.

ATTENTION: New material that we haven’t discussed. Try to figure it out on your own.

/*
  Prototype Event Dispatcher
*/
function EventDispatcher() {}
EventDispatcher.prototype.events = {};
EventDispatcher.prototype.addEventListener = function (key, func) {
    if (!this.events.hasOwnProperty(key)) {
        this.events[key] = [];
    }
    this.events[key].push(func);
};
EventDispatcher.prototype.removeEventListener = function (key, func) {
    if (this.events.hasOwnProperty(key)) {
        for (var i in this.events[key]) {
            if (this.events[key][i] === func) {
                this.events[key].splice(i, 1);
            }
        }
    }
};
EventDispatcher.prototype.dispatchEvent = function (key, dataObj) {
    if (this.events.hasOwnProperty(key)) {
        dataObj = dataObj || {};
        dataObj.currentTarget = this;
        for (var i in this.events[key]) {
            this.events[key][i](dataObj);
        }
    }
};


function Planet() {
    var stability = 100;
    var destroyed = false;
    this.takeHit = function () {
        stability--;
        switch (stability) {
            case 50:
                this.dispatchEvent("HIT_TAKEN", {
                    level: 50
                });
                break;
            case 20:
                this.dispatchEvent("HIT_TAKEN", {
                    level: 20
                });
                break;
            case 0:
                destroyed = true;
                this.dispatchEvent("HIT_TAKEN", {
                    level: 0
                });
                this.dispatchEvent("DESTROYED", {
                    sadface: true
                });
                break;
        }
    };
}
Planet.prototype = new EventDispatcher();


function SpaceWeapon() {
    var _this = this;
    var laserOn = false;
    var pointedAt;

    function turnOffLaser() {
        laserOn = false;
        console.log("That was the biggest explosion I've ever seen");
    }

    this.aimAt = function (planet) {
        setTimeout(function () {
            pointedAt = planet;

            planet.addEventListener("DESTROYED", turnOffLaser);

            _this.dispatchEvent("LOCKED_ON");
        }, 1000);
    };

    this.fire = function () {
        console.log("SpaceWeapon FIRING!!!!!!!");
        var failSafe = 0;
        if (pointedAt) {
            laserOn = true;
            while (laserOn === true) {
                failSafe++;
                if (failSafe > 1000) {
                    laserOn = false;
                    console.log("Damn this thing needs tweaked");
                }
                pointedAt.takeHit();
            }
        }
    };

}
SpaceWeapon.prototype = new EventDispatcher();



var Alderaan = new Planet();
Alderaan.addEventListener("HIT_TAKEN", function (e) {
    console.log("ALERT!!! Stability at " + e.level + "%");
});




var deathStar = new SpaceWeapon();
deathStar.addEventListener("LOCKED_ON", function (e) {
    console.log("locked on");
    deathStar.fire();

});

deathStar.aimAt(Alderaan);

I’ll save the explaination of the Prototypal Inheritance until another time. For now, enjoy blowing up Alderaan with the Death Star, and tweet your tests to @codecommando

2 Comments

  • Bart says:

    EventDispatcher.prototype.events = {};

    Are all listeners stored in the same object (since the runtime will find the same .events through prototype inheritance? Wouldn’t you want to have each instance with it’s own map of events+listeners?

  • Aaron says:

    Hey Bart,
    Yeah, you’re right. Here’s a demo of what you described. Even though test2 is firing the event the callback from test1 is fired. That’s bad.
    http://jsbin.com/ifafir/4/edit

    You can override the prototype’d events inside of your classes easy enough
    http://jsbin.com/ifafir/2/edit

    A simple this.events = {} creates a variable 1 level above the prototype’d object.

    It comes down to who is doing the coding. I like prototypes for some stuff, but hate them for others. Good eye though.

Leave a Reply