Saturday, May 7, 2011

Speeding Up Global Event Triggering in jQuery

Global event triggering implicates calling all the event handlers bound for a certain event, on all available elements. It is performed by calling jQuery.trigger() without passing any DOM element as context. It is nearly the same as calling trigger() on all the elements that have one or more bindings to the corresponding event, something like this:

jQuery('#a1,#a2,div.b5').trigger('someEvent');

Triggering globally is obviously simpler because you don’t need to know all the elements that need to be triggered. It’s quite useful for certain situations but can also be a slow process at times. Although it’s been optimized since jQuery 1.3, it still requires going through all the elements registered to jQuery’s event system. This can cause short (or not so short) hangs every time an event is triggered like this.

One possible solution is to have one or more global objects that will act as event listeners. These elements can be DOM elements or not. All global events will be bound and triggered on one of these elements.
Instead of doing something like this:

jQuery('#text1').bind('change-page', function(e, title){
jQuery(this).text( 'Page is ' + title );
});
jQuery('#text2').bind('change-page', function(e, title){
jQuery(this).text( 'At ' + title + ' Page' );
});
jQuery.trigger('change-page', 'Inbox');
you’d do something like this:
jQuery.page = jQuery({}); // Just an empty object
jQuery.page.bind('change', function(e, title){
jQuery('#text1').text( 'Page is ' + title );
});
jQuery.page.bind('change', function(e, title){
jQuery('#text2').text( 'At ' + title + ' Page' );
});
jQuery.page.trigger('change', 'Inbox');

The syntax seems pretty much the same, but each call to trigger won’t be iterating
jQuery’s data registry (aka jQuery.cache). Even if you decide to use a DOM element, the principle is the same. DOM elements can be more appropriate at times. If, for example, you’re creating a table-related plugin, then it’d make sense to use each element as an event listener.

The problem with DOM elements in many browsers is that they’re the main source of
memory leaks. Memory leaks occur when there are certain amounts of RAM memory
that cannot be freed by the JavaScript engine as the user leaves a page.
You should be much more careful about how you save data into the objects when you
use DOM elements. That’s why jQuery provides the data() method.
Still, I’d personally use regular JavaScript objects in most situations. You can add attributes and functions to them, and the likelihood (and magnitude) of memory leaks will be smaller.

This approach is faster. You will be always triggering events on single objects, instead of the n entries on jQuery.cache. The downside of this approach is that everyone needs to know the event listener object (jQuery.page in the example) in order to bind or trigger one of its known events. This can be negative if you’re aiming to keep your code encapsulated.*

The concept of encapsulation is highly enforced in object-oriented programming,
where this is one of the things you should be very cautious about.
This is generally not such a great concern with jQuery programming, because it is not
object oriented and most users don’t get too worried about code encapsulation. Still,
it’s worth mentioning.

The listener objects mentioned don’t have to be simple dummy objects with nothing
but bind(), unbind(), and trigger() (as far as we’re concerned).
These objects could actually have methods and attributes that would make them much more useful.

The only problem, though, is that if we do something like this:

jQuery.page = jQuery({ number:1 });

to access the number attribute, we would be forced to do this:

jQuery.page.number; // undefined
jQuery.page[0].number; //

This is how jQuery works on HTML nodes and anything else.
But don’t give up on me yet! It’s easy to work around this. Let’s make a small plugin:

(function( $ ){
// These methods will be copied from jQuery.fn to our prototype
var copiedMethods = 'bind unbind one trigger triggerHandler'.split(' ');
// Empty constructor
function Listener(){
};
$.each(copiedMethods, function(i,name){
Listener.prototype[name] = $.fn[name];
});

// Our "jQuery.fn.each" needs to be replaced
Listener.prototype.each = function(fn) {
fn.call(this);
return this;
};
$.listener = function( data ){
return $.extend(new Listener(), data);
};
})( jQuery );

Now we can create objects that will have all the jQuery methods we need that are related to events, but the scope of the functions we pass to bind(), unbind(), etc., will be the object itself (jQuery.page in our example).
Note that our listener objects won’t have all jQuery methods but just the ones we
copied. While you could add some more methods, most of them won’t work. That
would require a more complex implementation; we’ll stick to this one, which satisfies
our needs for events.

Now that we have this mini plugin, we can do this:

jQuery.page = jQuery.listener({
title: 'Start',
changeTo: function( title ){
this.title = title;
this.trigger('change');
}
});
jQuery.page.changeTo('Inbox');

Because you can now access the object from within the handlers, using the this, you
don’t need to pass certain values like the title as arguments to the handler. Instead, you can simply use this.title to access the value:

jQuery.page.bind('change', function(e){
jQuery('#text1').text( 'Page is ' + this.title );
});