Javascript Hijinks - Events

Jul 06, 2007 09:31

I've been writing metric assloads of JavaScript lately. For work, I've built an AJAX layer to speed up our horrifyingly slow and complex web-app. For home, I've taken what I learned by doing that and built an AJAX data-crunching-pipelining framework. I'll post more about that later- when I've got a clearer picture of how, exactly, I want to use it1.

Along the way, I've been learning a lot about what exactly prototype inheritance means. For developers coming from a true OOP background, like myself, it really requires some changes in the way you think about programming. The more I learn about JavaScript, the more I like it- it's an incredibly powerful and mature language- a far cry from what it was back in 1994.

Okay, so onto events, which demonstrates some of the neat features of prototype inheritance. I was writing a class called AsyncManager. The class wraps around the XMLHTTPRequest object and provides its own callbacks for when the request is complete. When the request completes, it notifies the appropriate objects that it's done. That wasn't a problem- but I wanted some objects to be aware when the request started. For example- when the request starts, I want to display "LOADING...", but when it finishes, I'll display the results of processing.

The best way to do this is to fire off events. My first instinct was to add some methods to the class that would behave like so:
  1. registerStartListener(callback): A client class- one that wants to know when a request starts supplies a callback function (functions are variables in JavaScript). The AsyncManager class dumps this function into an array.
  2. fireStartEvent(): My AsyncManager calls this method when a request starts. This function loops across every element in the array of callbacks and invokes that method.

That works, but it's really annoying. For one, every class that I want to fire off events needs to replicate that code. And, if I want to have more than one event (like an event to notify the world that I'm done with the request), I have to duplicate nearly identical code. That's bad. Well, I can take advantage of the fact that JavaScript doesn't care if I access properties directly or via [] notation. In other words, I could solve the second problem with code like this:

this.declareEvent = function(name)
{
this[name] = new Array();
}

this.registerListener = function(eventName, callback)
{
this[eventName].push(callback);
}
this.fireEvent = function(eventName)
{
for (var i = 0; i < this[eventName].length; i++)
{
this[eventName][i]();
}
}
(That code should have type-safety checks, I know)
Okay, that solves the second problem - my class can now publish events using "declareEvent". Even better- that can be done at runtime, allowing dynamic publishing of events (perhaps I should add some "un-publishing" too?).

What if I wanted to solve that first problem though? The code I've got here will be identical for every class that fires off events. Using inheritance is out- while it could work, it's a bad design. It's a very natural desire to want to mix and match event-firing features in different levels of the inheritance hierarchy2.

JavaScript offers "swiss" inheritance. This lets you do multiple inheritance, but to dodge name collisions, it requires that you explicitly specify which methods you're inheriting. That's strong coupling, and it's bad.

But the JavaScript prototype gives us an interesting opportunity to implement a "mix-in" architecture. My final solution was to create a function that takes a class as its input and uses the class's prototype to attach functions to all instances of the class.

function EventSource(className)
{
className.prototype.declareEvent = function(eventName)
{
if (eventName)
{
this[eventName] = new Array();
}
}

className.prototype.registerListener = function(eventName, callback)
{
if (this[eventName] && callback)
{
this[eventName].push(callback);
}
}

className.prototype.fireEvent = function(eventName, e)
{
if (this[eventName])
{
var handlers = this[eventName];
for (var i = 0; i < handlers.length; i++)
{
handlers[i](e);
}
}
}
}

Now, to take my AsyncManager class and make it capable of firing events and registering listeners, all I need to do is: EventSource(AsyncManager);. Now, magically, all instances of AsyncManager have this ability. And I can add that ability to any class by merely calling the EventSource function. And- if I add functions to EventSource, those functions are automatically added to all of the affected classes. And the only strong coupling (the only function AsyncManager, or any event-firing class calls) is on the "declareEvent" and the "fireEvent" method. Those two methods are unlikely to change- in fact, they're the whole point of doing this!

This is, as best I can tell, the closest you'll get to Aspect Oriented programming in JavaScript.

1Kids, don't write software if you don't know what it's for. You should always have business requirements in hand before writing a line of code. Coding for no damn reason should only be done by trained professionals.

2Inheritance is a very tempting hammer. Once you have it, and understand it, many problems start to look like nails. There are many reasons to not us inheritance- the most important ones all have to do with losing flexibility and promoting strong coupling in your code. Coupling is bad. Flexibility is good. Make sure inheritance is really the best solution before implementing it.

EDIT::
The sequence diagram would probably be easier to understand if I included the XMLHTTPRequest object- the actual guts that makes this all work. Of course, I find that object clumsy to work with, hence my high-level wrappers. By the time I'm done with my pipeline architecture, you'll set up a pipeline of processors, say, "Get the page, run it through this pipeline" and voila! You have results.
Classes:


Request Sequence:


programming, work, javascript

Previous post Next post
Up