Dev Logging

printf("does this work\n");
in

A Lightweight Event Framework in JavaScript

I’ve been working on a project recently that’s very JavaScript heavy. This probably makes some of you cringe, but I think JavaScript is probably one of my favorite languages. True, it has a lot of terrible quirks and gotchas, but the core of the language is light, powerful, and expressive. For an example of this, I’m going to talk a little bit about a lightweight event framework I built in JavaScript for this project.

When I say “events,” I’m not talking about the DOM. This is obviously a solved problem. I’m talking about events in a more general sense; your application grows to a certain size, and it becomes convenient to pass messages around between components. Along the way, we’ll need to learn about a few (ok, several) of JavaScript’s foibles and a few advanced techniques. Let’s get started:

var CLARITY = {
    events: {}
};

Here I’m defining a global object (CLARITY) via object literal syntax. Object literal syntax allows you to define objects inline as a list of key-value pairs. Our object has one property (events), which is itself an empty object literal. This is one of my favorite features of JavaScript, since it gives you a lot of flexibility. You may be wondering why we need to wrap everything in our global object. This is because of a design flaw in JavaScript. It has no concept of namespaces, so everything defined at the top level goes on the global object (window). We can restrict our footprint on the global object by defining our own app-level object to wrap everything. This becomes really important when you start using third party libraries, since you don’t want to clobber someone else’s globals and introduce any subtle bugs.

Let’s flesh out our events object:

var CLARITY = {
    events: {
        subscribe: function(eventName, callback) { },
        publish: function(eventName) { }
    }
};

We’ve added two functions to events: subscribe and publish. Note that we’ve done this via anonymous functions, another really powerful feature of JavaScript. In JavaScript, functions are first-class. This means a function can be treated like any other kind of value. This is going to define our base API for our event framework. You can subscribe by passing an event name and a callback function, and you can publish an event with just the event name. Let’s describe our subscribe function:

var CLARITY = {
    events: function() {
        var events = {};
        return {
            subscribe: function(eventName, callback) {
                events[eventName] = events[eventName] || [];
                events[eventName].push(callback);
            },
            publish: function(eventName) { }
        }
    } ()
};

This is probably the most complicated single step, so don’t worry if it looks a little alien. First, notice that events is no longer a simple object literal. We now have an anonymous function that returns an object literal. This might seem confusing, but notice that it is invoked immediately. So events is really still being assigned the same object literal. We’ve added this extra piece of indirection to take advantage of closures. We’ve defined an empty object called events right inside the function. In JavaScript, scoping is at the function (rather than block) level. When we return the object literal, any references to objects defined within the function are captured. This means our events object lives on inside our subscribe-publish object, but no one else has direct access to it. This is a really powerful concept, and it gives us a lot of power over how we use objects in JavaScript.

Let’s turn our attention to the subscribe function. Our general strategy is going to be to use events as a hash table mapping event names to arrays of callback functions. This way, we can have multiple callbacks for a given event. We can easily accomplish this, because all objects in JavaScript can be thought of as hash tables. In fact, of these two lines, the second is merely syntactic sugar for the first:

events['something'] = function() { };
events.something = function() { };

So the first line of subscribe is checking to see whether a property with this event name exists on events. If not, it assigns an empty array literal. We do this by using the or operator. If the first value is undefined, it evaluates to false, and we take the second value. The or operator is often called the guard operator when used like this. Now that we know there’s an array in place for us to use, we push our callback onto it. This is pretty much it for our subscribe function.

Publish is even simpler:

var CLARITY = {
    events: function() {
        var events = {};
        return {
            subscribe: function(eventName, callback) {
                events[eventName] = events[eventName] || [];
                events[eventName].push(callback);
            },
            publish: function(eventName) {
                var i, callbacks = events[eventName];
                if (callbacks) {
                    for (i = 0; i < callbacks.length; i++) {
                        callbacks[i]();
                    }
                }
            }
        }
    } ()
};

We check to make sure the event exists, then iterate over the array and invoke all our callbacks. Fairly straightforward. We can check that this works with the following simple code:

CLARITY.events.subscribe('something', function() {
    alert('event received!');
});

CLARITY.events.publish('something');

Which produces the following result:

image

Sweet. This works for simple cases, but it would be a lot more useful if we could actually pass some payloads around. It’s not immediately clear how to go about this, but there are ways:

var CLARITY = {
    events: function() {
        var events = {};
        return {
            subscribe: function(eventName, callback) {
                events[eventName] = events[eventName] || [];
                events[eventName].push(callback);
            },
            publish: function(eventName) {
                var i, callbacks = events[eventName], args;
                if (callbacks) {
                    args = Array.prototype.slice.call(arguments, 1);
                    for (i = 0; i < callbacks.length; i++) {
                        callbacks[i].apply(null, args);
                    }
                }
            }
        }
    } ()
};

Notice the reference to arguments that seems to come out of nowhere. Arguments is actually an implicit local variable in every function. It’s an array-like object that holds each argument passed to the function. This allows for some interesting metaprogramming possibilities. JavaScript doesn’t enforce the defined parameters for function calls, so you can pass as many (or as few) as you’d like. They’ll be bound to the defined parameters in the order they’re passed, until you run out of defined parameters. If you want any other parameters, you’ll need to use arguments.

The phrase “array-like object” may stand out. Because of a strange design decision, arguments is not an array, but an object with properties named 0, 1, 2, etc., and a property named length. The first line inside the if statement is a sort of hack to make an array out of arguments. We can call the slice function (which takes a subset from an existing array) directly from Array’s prototype with the call function. These are equivalent:

var array = [1, 2, 3, 4], slice;
slice = array.slice(2);
slice = Array.prototype.slice.call(array, 2);

When we call slice directly from our array instance, the instance is the context of the function invocation. This means that “this” will be bound to the instance. If we were to call slice directly from the prototype:

slice = Array.prototype.slice(2);

“this” would be bound to the global object. (For some reason . . .) The call function allows us to specify a binding for “this” followed by the normal arguments the function is expecting. Arguments is not an array, but it has enough similar properties that the function behaves correctly and returns a new array. Note that we specify 1 as our starting index, so that args is an array of all arguments passed in other than the event name.

Now that we have an array of payload arguments, we need a way to send it to our callback. We could do this:

callbacks[i](args);

But this is less than ideal. This would require our callback function to treat its expected payload as an array, even if it only wanted one value. Fortunately, we have the apply function.

Apply is a sister function to call. It’s the same insofar as allows you to specify a scope binding for the function, but it handles the passed arguments differently. Whereas call expects the parameters separately, apply takes an array. (Well, actually, it takes an array-like object, but arrays are extremely array-like.) The values in the arguments array are then bound to the defined parameters of the function in question, so you can use them as expected. Take this example:

CLARITY.events.subscribe('something', function(one, two) {
    alert('event received with payload: ' + one + ' and ' + two);
});

CLARITY.events.publish('something', 'does this work?', 'you know it!');

This yields the desired result:

image

Note that the parameters don’t need to be strings; they can be any kind of objects:

CLARITY.events.subscribe('something', function(data) {
    alert('event received with payload: ' +
        data.one + ' and ' + data.two);
});

CLARITY.events.publish('something', {
    one: 1,
    two: 'two'
});

With result:

image

I’m not sure if stuff like this already exists. Searching for JavaScript events obviously gets you a lot of stuff about DOM events. Anyway, I thought this was kind of cool, so hopefully someone can get some ideas from it.

Comments

David Orchard said:

What about jQuery events for comparison?

# June 24, 2009 5:07 PM

sdevlin said:

Interesting point, David. I knew someone had to have done application (i.e. non-DOM) events in JavaScript. Looking at their interface, there are a couple things I don't like. I'll write a blog post tonight to explain.

Thanks for pointing that out!

# June 25, 2009 7:11 AM

Ancora Imparo said:

A reader comment on my last post brought it to my attention that support for custom events actually already

# June 25, 2009 7:51 PM

Dev Logging said:

Regular readers will notice that I’m quite taken with JavaScript recently . I’ve been looking at using

# August 21, 2009 4:33 PM