Event Dispatcher

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.

Example 1: Event Dispatcher Object

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

    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.