Modern web applications behave a lot like desktop applications, with nearly asynchronous updates, pre-caching of events and DOM, animations, drag and drop, etc. However, all these features come with a price, and a web application developer constantly considers add a new feature versus degrading site performance. Todays topic illustrates how you can reduce the cost of events leveraging an event dispatcher, ‘EventDispatcher’.
From Widipedia:
Multiple dispatch or multimethods is the feature of some object-oriented programming languages in which a function or method can be dynamically dispatched based on the run time (dynamic) type of more than one of its arguments.
The dynamic type used by EventDispatcher is special command classes starting with ‘com_’ and ending with the event id, so the class ‘com_doSomething’ would dispatch the ‘doSomething’ event (as long as it is defined). A public method exists on the EventDispatcher object with allows for event registration, so the dispatcher knows how to delegate a given event id. The first time a type of event (such as ‘click’) is registered, a new listener is added to the document. This means that for each type of event there will be only 1 callback, even if the page has hundreds of events of that type registered. When one of the registered event types fires on the page, the target element is checked for command classes and all of its ancestor elements, dispatching as needed.
Core.Util.EventDispatcher = (function() {
// local variables
var callbackMap = {},
doc = document,
F = function() {},
rx = /\\bcom_\\w+\\b/g,
that = null,
YUE = YAHOO.util.Event;
// event namespace
var E = {
dispatcher: function(e) {
var node = YUE.getTarget(e);
// simulate bubbling
while (node && node !== doc) {
var coms = node.className.match(rx);
// not matched
if (null === coms) {
// not found, do nothing for now
}
// command class exists
else {
var i = 0, j = 0;
// iterate on matching commands
for (; i < coms.length; i += 1) {
var id = coms[i].replace(/com_/, ''),
carr = callbackMap[e.type][id];
// object for command exists, command could be for another event
if (carr && carr.length) {
// iterate on command callbacks
for (j = 0; j < carr.length; j += 1) {
var o = carr[j],
args = [e];
if (o.eventFx) {o.eventFx(e);} // event stop events
o.callback.apply(o.scope, args.concat(o.arguments));
}
}
}
}
node = node.parentNode;
}
}
};
// public interface
F.prototype = {
register: function(type, o) {
// check for required
if (! (type && o && o.id && o.callback)) {
alert('Invalid regristration to EventDispatcher - missing required value, see source code.');
}
// allows for lazy-loading of events
if (! callbackMap[type]) {
callbackMap[type] = {};
YUE.on(doc, type, E.dispatcher);
}
if (! callbackMap[type][o.id]) {callbackMap[type][o.id] = [];}
if (! o.scope) {o.scope = window;}
if (! o.arguments) {o.arguments = [];}
if (! YAHOO.lang.isArray(o.arguments)) {o.arguments = [o.arguments];} // support arguments that are non arrays
callbackMap[type][o.id].push(o);
}
};
that = new F();
return that;
})();
The EventDispatcher object is a static object that must be registered with for dispatching to occur. The ‘register’ method requires 2 parameters: the first is the name of the event, and the second is an object (Dispatcher Object), which must at least have a unique event id (this is the second half of your command class ‘com_eventId’) and a ‘callback’ function. In addition, the ’scope’ of the callback function can be can specified, as well as any additional ‘arguments’ to be passed into the callback function. The registration function first validates these parameters, then determines if the event type is listened for; attaching the event and initializing the callback map for that event type as necessary. When the event id is not yet mapped, a new array is added for that event id and the second parameter object is pushed onto this array (we use an array, because many elements can share the same command class).
Later when the registered event is triggered, the generic dispatcher method is called. This method first gets the event target, then iterates up the DOM until the document or NULL is reached. During each iteration the DOM node is checked to see if it contain a command class (using a simple regex). Any command class found is compared to those mapped in the ‘callbackMap’ and when a mapping is found, it is iterated on, finding each Dispatcher Object and executing the defined callback. The ‘eventFx’ is another option defined on the Dispatcher Object and can be any method used to stop events (such as ’stopEvent’ or ’stopPropogation’). Lastly, the callback function is executed using the ‘apply’ method, so that the scope can be overridden and the arguments defined in the Dispatcher Object will become the parameters of the callback function (first argument is the event, followed by the values in ‘arguments’).
Here is a test page, so you can experiment with the dispatcher. Mostly it is meant to show that the dispatcher works in many different kinds of situations.