Decoupling JS code - Events
This post is the 3rd in a series of posts about decoupling JS code. You can find the table of contents here
In the previous post we disscussed how we can leverage JS's built in mechanisms to our advatages. In this post we will take these tools to create even stronger patterns.
I think one of the best practices JS brought upon us was event-based programming. When I speak of building event-based code, I'm not talking about the DOM exclusively, although it (alongside AJAX) is probably the main contributor to the wide acceptance and understanding of its core principles.
Events allow us to create hooks between different parts of our code without creating direct dependencies. It's a power that comes to us in the courtesy of JS's amazing lambda implementation (as I already described in my previous post). I believe writing event-based code is one of best things Mootools thought me, and all my code is knee deep with events firing all around. (I've actually written a post about this a while back).
Let's take the example from our example from previous article:
var List = new Class({
options : {
beforeOpenFunction : function(el){}
' openFunction : function(el){}
}
, initialize : function(el,opts){
this.setOptions(opts);
this.element = $(el);
}
, open : function(){
this.options.beforeOpenFunction(this.el);
this.element.setStyle('display','block');
this.options.openFunction(this.el);
}
});
var list = new List('someid',{
beforeOpenFunction : function(el){
el.setStyle('height',0);
}
, openFunction : function(el){
el.tween('height',0,100); //tween is an animating method. this one will animate the element's height between 0 and 100 pixels
}
});
Here, an object allows external user-objects to change (or supplement) certain aspects of its behavior in a determined way. One key aspect of this pattern though is that only one function can register to the mechanism at a time. For certain scenarios this is quite important, but for other it is a limitation, and this is where events come in. Before we go on, I want to define what actions I think should be candidates for events:
- Actions that represent a change of state. This can be a successful completion of a request, or a user interaction.
- The mentioned action happens outside the flow of the program. For example, an 'init' event is usually unnecessary since in most cases it activates right after an object finishes its construction (at which point the code that initialized the Object would continue running anyway.
The reason we find so much use of event based coding on the web is that a lot of our code runs outside of the initial program flow – such as a click on an element. Writing event-based code simply means that we allow external objects to listen to such behaviors of our Object.
The event pattern is taken to its limit with the pub/sub pattern, or in the case of a domain observer/dispatcher. In these cases, one single domain-wide object is in charge of most (if not all) the interaction between the object of that domain, using a defined API. So for example:
function Obj1(observer){
var $this = this;
this.observer = observer;
observer.addEvent('some-message',function(msg){
$this.perfromAction(msg);
});
}
function Obj2(observer){
this.observer = observer;
}
Obj2.prototype = {
performAction : function(){
this.observer.fireEvent('some-message','abc def');
}
};
var ob1 = new Obj1(document.Observer)
, ob2 = new Obj2(document.Observer);
ob2.performAction();
Obj2 has no idea if its action affects other objects, not does it care. Obj1 makes no implicit use of Obj2, and doesn't even care if Obj2 is the one sending him messages.
There is no reason why you should not be using event based programming right now. All large libraries support some sort of implementation for writing events (some better than others), and writing an implementation of your own is quite easy.
But, beware – here's the catch - the way most libraries implement their costume events is broken – their implementation were not designed as a means of decoupling. What do I mean? As far as I've been able to see, all libraries fire their costume events by iterating over a collection of attached functions (here's Mootools' snippet). Why is this bad? Let's look at this snippet
This is a basic DOM concept – each function is executed within its own context. Breaking one function will not prevent the rest from running. This is a crucial concept when we start creating site-wide costume events – such as DOMReady:
Almost 90% of code examples I see today run within a DOMReady context (I actually never use the DOMReady event, but that’s for another article). That can be fine, but what happens if we add a widget to our site, and it breaks? I'll tell you what – your site will break. And that's unbearable – especially if that widget's code is already minified. This limitation is actually a pitfall for other techniques I'll be talking about, because as I've mentioned earlier, the idea of passing a lambda to an object for use at later time is one of the most basic tools we have to decouple our code, and this tool almost completely crumbles by that simple flaw – an object's behavior shouldn't be affected by the fact that some other object – which has nothing to do with it – broke.
But what can we do? Well, quite a lot actually. I've seen several techniques for solving this pickle - one goes as far as creating a script element and injecting the execution into it. My favorite technique however is using setTimeout. It's actually quite simple, and quite powerful. When setting a timeout without any delay (passing 0), we actually create a fully synchronous execution, with one big difference – when we use setTimeout, we, amongst other things, tell the interpreter that that piece of code is independent – it should have its own execution context. And that is exactly what we need!
So, if we take the fireEvent implementation from Mootools –
events.each(function(fn){
if (delay) fn.delay(delay, this, args);
else fn.apply(this, args);
}, this);
All we need to do is change 2 lines of code –
events.each(function(fn){
if (delay==undefiend) delay = 0;
fn.delay(delay, this, args); //fn.delay is a synonym for setTimeour(fn)
}, this);
And that's it. Now, obviously, playing with the core of a library can have unforeseen consequences, especially when it comes to something so deeply integrated into the heart of the library, and so I created my own version – one that allows you to safely create domain observers (such as my history manager which already uses parts of that code) which would allow you to elevate your use of event. Using such a tool will allow you, for example, to introduce new pieces of code into a running application without fearing everything will break around them.