A Lightweight Event Framework in JavaScript

Posted by: Clarity Blogs: ASP.NET, on 24 Jun 2009 | View original | Bookmarked: 0 time(s)

Ive been working on a project recently thats 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, Im going to talk a little bit about a lightweight event framework I built in JavaScript for this project.

When I say events, Im not talking about the DOM. This is obviously a solved problem. Im 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, well need to learn about a few (ok, several) of JavaScripts foibles and a few advanced techniques. Lets get started:

var CLARITY = {
    events: {}
};

Here Im 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 dont want to clobber someone elses globals and introduce any subtle bugs.

Lets flesh out our events object:

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

Weve added two functions to events: subscribe and publish. Note that weve 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. Lets 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 dont 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. Weve added this extra piece of indirection to take advantage of closures. Weve 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.

Lets 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 theres 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++) {
                        callbacksIdea();
                    }
                }
            }
        }
    } ()
};

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. Its 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++) {
                        callbacksIdea.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. Its an array-like object that holds each argument passed to the function. This allows for some interesting metaprogramming possibilities. JavaScript doesnt enforce the defined parameters for function calls, so you can pass as many (or as few) as youd like. Theyll be bound to the defined parameters in the order theyre passed, until you run out of defined parameters. If you want any other parameters, youll 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 Arrays 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:

callbacksIdea(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. Its 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 dont 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

Im 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.

Advertisement
Free Agile Project Management Tool from Telerik
TeamPulse Community Edition helps your team effectively capture requirements, manage project plans, assign and track work, and most importantly, be continually connected with each other.
Category: Events | Other Posts: View all posts by this blogger | Report as irrelevant | View bloggers stats | Views: 1510 | Hits: 18

Similar Posts

  • The Telerik CAB Enabling Kit and SCSF - Tutorial 5: The RadPanelBar UIExtensionSite more
  • Why not Classic (Legacy) ASP? more
  • Creating a Filtering User Interface With jQuery In a Web Forms Application: Part 1 more
  • StreamInsight more
  • Creating Objects with Observable Properties in JavaScript more
  • Who Activates, Displays, and Closes Screens? more
  • WPF Release History : Q2 2009 (version 2009.2.701) more
  • Event Aggregation with jQuery more
  • Web.UI ASP.NET Grid: Synchronize Checkbox States with Row Selection more
  • Tech Ed 2009 Cometh - EF and more more

News Categories

.NET | Agile | Ajax | Architecture | ASP.NET | BizTalk | C# | Certification | Data | DataGrid | DataSet | Debugger | DotNetNuke | Events | GridView | IIS | Indigo | JavaScript | Mobile | Mono | Patterns and Practices | Performance | Podcast | Refactor | Regex | Security | Sharepoint | Silverlight | Smart Client Applications | Software | SQL | VB.NET | Visual Studio | W3 | WCF | WinFx | WPF | WSE | XAML | XLinq | XML | XSD