Event Internals in jQuery - Part Two

Published on Thursday, 10 March 2011 by Russ Cam

Part one of jQuery Event internals looked at the DOM Event specifications, the history of the Document Object Model (DOM) event models and reasoning behind why jQuery has an event system. In the second part of this series, we'll be looking at the jQuery.event object and jQuery.Event constructor function, both of which play a pivotal role in managing events. Previously, we looked at the bind() method (and the related specific event handler binding methods such as click(), keyup(), etc) and saw that bind() eventually calls jQuery.event.add, so let's start by looking at that.

jQuery.event.add

It helps to have the source to hand when discussing the internals, so here is the source for jQuery.event.add from jQuery 1.6.1:

/*!
 * jQuery JavaScript Library v1.6.1
 * http://jquery.com/
 *
 * Copyright 2011, John Resig
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 * Copyright 2011, The Dojo Foundation
 * Released under the MIT, BSD, and GPL Licenses.
 *
 * Date: Thu May 12 15:04:36 2011 -0400
 */

// ... other code omitted, starting at line 2511 of source 

jQuery.event = {

    add: function( elem, types, handler, data ) {
        if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
            return;
        }

        if ( handler === false ) {
            handler = returnFalse;
        } else if ( !handler ) {
            return;
        }

        var handleObjIn, handleObj;

        if ( handler.handler ) {
            handleObjIn = handler;
            handler = handleObjIn.handler;
        }

        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }

        var elemData = jQuery._data( elem );

        if ( !elemData ) {
            return;
        }

        var events = elemData.events,
            eventHandle = elemData.handle;

        if ( !events ) {
            elemData.events = events = {};
        }

        if ( !eventHandle ) {
            elemData.handle = eventHandle = function( e ) {
                return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
                    jQuery.event.handle.apply( eventHandle.elem, arguments ) :
                    undefined;
            };
        }

        eventHandle.elem = elem;

        types = types.split(" ");

        var type, i = 0, namespaces;

        while ( (type = types[ i++ ]) ) {
            handleObj = handleObjIn ?
                jQuery.extend({}, handleObjIn) :
                { handler: handler, data: data };

            if ( type.indexOf(".") > -1 ) {
                namespaces = type.split(".");
                type = namespaces.shift();
                handleObj.namespace = namespaces.slice(0).sort().join(".");

            } else {
                namespaces = [];
                handleObj.namespace = "";
            }

            handleObj.type = type;
            if ( !handleObj.guid ) {
                handleObj.guid = handler.guid;
            }

            var handlers = events[ type ],
                special = jQuery.event.special[ type ] || {};

            if ( !handlers ) {
                handlers = events[ type ] = [];

                if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
                    if ( elem.addEventListener ) {
                        elem.addEventListener( type, eventHandle, false );

                    } else if ( elem.attachEvent ) {
                        elem.attachEvent( "on" + type, eventHandle );
                    }
                }
            }

            if ( special.add ) {
                special.add.call( elem, handleObj );

                if ( !handleObj.handler.guid ) {
                    handleObj.handler.guid = handler.guid;
                }
            }

            handlers.push( handleObj );

            jQuery.event.global[ type ] = true;
        }

        elem = null;
    },

// ...

The function kicks off with checking the nodeType of the element to which the event handler will be bound. If the element is a text node (nodeType 3) or a comment node (nodeType 8) then no handler will be bound and the function simply exits at this point. Then follows a check to detect if the element is the window object and to set it to window if true to prevent Internet Explorer from erroneously cloning it when passing it around. If false is passed in for the handler function then the handler is set to a function that returns false; if no handler is passed in, the add function simply returns. Essentially, there are a number of defensive steps here to avoid doing work unnecessaily and to handle various shorthands for setting up a handler function.

Next comes a check for handler.handler. The handler function is given a unique id using jQuery.guid, a number counter that is held in a closure and incremented on each call. jQuery._data(elem) is used to initialize the internal event structure for the element. Internally, this uses jQuery.data to initialize a cache for the element; if the element is a JavaScript object, then a cache object is initialized on the object itself using a property name that is constructed by the instance of the jQuery script that runs, using the jQuery.expando: property. That's defined as such

// line 1384 from the source
jQuery.extend({
    cache: {}, // this is the internal cache object

    uuid: 0,

    // this is essentially a unique id for the instance of jQuery
    // such that two instances of jQuery on the same page do not have the 
    // same expando
    expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),

//...

jQuery.expando serves the purpose of allowing more than once instance of the jQuery library to run on the page, by giving each instance of jQuery a key to use when adding properties to objects and element nodes.

In the event structure initialization, If the element for which we're setting up event handling is not a JavaScript object but a DOM node then values are assigned to properties on an object created inside of $.cache; the DOM node is given a unique id using jQuery.uuid, another number counter that is incremented on each call.

Once all of the event structure initialization has been done, jQuery._data returns the cache object for the element. If no object is returned, this indicates that the element in question is not one that can have events bound to it and so the code simply returns at this point. Here comes the interesting part. Two items are retrieved from the cache object (or created if they don't already exist), events and handle; events is an object with properties that match bound event types, with each property assigned an array of event handler functions to run in response to that event type being raised on the element in question. The structure of events is as follows

events = {
    click :     [{ handler : function (e) { /* click event handler 1 */ }, data : { item: 'item' } },
                 { handler : function (e) { /* click event handler 2 */ }, data : undefined }],
    keyup:      [{ handler : function (e) { /* keyup event handler 1 */ }, data : { additionalData: [1, 2, 3] } }],
    mouseover:  [{ handler : function (e) { /* mouseover event handler 1 */ }, data : undefined }],
    mouseout :  [{ handler : function (e) { /* mouseout event handler 1 */ }, data : undefined }]
};

handle is a function that, under normal circumstances (i.e. we're not dealing with special events or cross-browser workarounds for specific events), returns the result of applying the jQuery.event.handle function with the context set to the element on which the event is bound. This is what enables the this keyword to refer to the element on which the event is raised inside of the event handler function. We'll look at the jQuery.event.handle function shortly.

Once the events object is retrieved from the cache, the space separated event types string passed in the bind() function call is split and iterated over; in each iteration, a new object containing a handler property with a value set to the function passed into the bind() call and a data property set to the data passed into the bind() call is pushed onto an array set against the property name matching the event type on the events object from the cache. Namespaced events are handled by the addition of a namespace property on the object pushed onto the array. If this is the first event handler being bound for the element for the event type, then the handle function is registered with the browser as an event listener using element.addEventListener if supported, or element.attachEvent otherwise.

The important point to take away here is that only one function is registered as an event handler with the browser per element, per event type. If I were to guess as to why this is done it would be to

  1. allow better control over the function context (what this refers to) inside event handler functions
  2. allow patching/augmentation of the browser event object before passing to event handlers to normalize event properties across browsers.
  3. keep track of how many event handler functions have been bound and allow for easier clean up when a new page is requested, to avoid browser memory leaks (here's looking mainly at you, IE).

Finally, a property matching the event type is set to true on the jQuery.event.global object. This is to allow global triggering of event handlers for specific event types.

With all of this in mind, one can inspect the events set up on an element using jQuery using

var events = $('selector-for-element').data('events');
// events now contains a reference to the events object held in the cache for “selector-for-element”, 
// or undefined if there are no event handlers bound (via jQuery) for the element

This will return the event object referred to earlier, the one containing properties with names that match the event types to which event handler functions have been bound. If you’re a fan of Firefox, there is a add-on for Firebug called FireQuery that shows what is in the cache for each element in the HTML tab of Firebug. What’s nice about this is that the cache object links can be clicked, which will take you to the DOM tab and allow you to inspect the object further.

A word of caution here - This internal event structure is undocumented in the jQuery source documentation and hence makes it subject to change. So, be particularly careful if you intend to rely on internal event structure inspection in production code as this may break when upgrading to a newer version of the jQuery library.

Now we know how jQuery.event.add works, let's look at the function that performs the opposite operation.

jQuery.event.remove

Once you know how jQuery.event.add works, there's not really much to discuss with jQuery.event.remove; it is a function called by unbind() and does pretty much the reverse of jQuery.event.add, removing handlers in the cache for the element(s) on which unbind() is called. If there are no more handler functions in the cache for a particular event type for an element, then the event listener registered with the browser is removed via element.removeEventListener if supported, element.detachEvent otherwise.

jQuery.event = {

    //... starting at line 2640 in the source

    remove: function( elem, types, handler, pos ) {
        if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
            return;
        }

        if ( handler === false ) {
            handler = returnFalse;
        }

        var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
            elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
            events = elemData && elemData.events;

        if ( !elemData || !events ) {
            return;
        }

        if ( types && types.type ) {
            handler = types.handler;
            types = types.type;
        }

        if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
            types = types || "";

            for ( type in events ) {
                jQuery.event.remove( elem, type + types );
            }

            return;
        }

        types = types.split(" ");

        while ( (type = types[ i++ ]) ) {
            origType = type;
            handleObj = null;
            all = type.indexOf(".") < 0;
            namespaces = [];

            if ( !all ) {
                namespaces = type.split(".");
                type = namespaces.shift();

                namespace = new RegExp("(^|\\.)" +
                    jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)");
            }

            eventType = events[ type ];

            if ( !eventType ) {
                continue;
            }

            if ( !handler ) {
                for ( j = 0; j < eventType.length; j++ ) {
                    handleObj = eventType[ j ];

                    if ( all || namespace.test( handleObj.namespace ) ) {
                        jQuery.event.remove( elem, origType, handleObj.handler, j );
                        eventType.splice( j--, 1 );
                    }
                }

                continue;
            }

            special = jQuery.event.special[ type ] || {};

            for ( j = pos || 0; j < eventType.length; j++ ) {
                handleObj = eventType[ j ];

                if ( handler.guid === handleObj.guid ) {
                    if ( all || namespace.test( handleObj.namespace ) ) {
                        if ( pos == null ) {
                            eventType.splice( j--, 1 );
                        }

                        if ( special.remove ) {
                            special.remove.call( elem, handleObj );
                        }
                    }

                    if ( pos != null ) {
                        break;
                    }
                }
            }

            if ( eventType.length === 0 || pos != null && eventType.length === 1 ) {
                if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
                    jQuery.removeEvent( elem, type, elemData.handle );
                }

                ret = null;
                delete events[ type ];
            }
        }

        if ( jQuery.isEmptyObject( events ) ) {
            var handle = elemData.handle;
            if ( handle ) {
                handle.elem = null;
            }

            delete elemData.events;
            delete elemData.handle;

            if ( jQuery.isEmptyObject( elemData ) ) {
                jQuery.removeData( elem, undefined, true );
            }
        }
    }
}

There's not really much more to discuss here, so we'll move on.

jQuery.event.handle

As highlighted before, only one handler function is registered with the browser per element, per event type. The handler function in question is jQuery.event.handle and its responsibility is to execute each of the handler functions in the array assigned to the property name on the events object held in the cache for the element in question, whose name matches the name of the event raised.

jQuery.event = {    

    //... starting at line 2903 in the source

    handle: function( event ) {
        event = jQuery.event.fix( event || window.event );

        var handlers = ((jQuery._data( this, "events" ) || {})[ event.type ] || []).slice(0),
            run_all = !event.exclusive && !event.namespace,
            args = Array.prototype.slice.call( arguments, 0 );

        args[0] = event;
        event.currentTarget = this;

        for ( var j = 0, l = handlers.length; j < l; j++ ) {
            var handleObj = handlers[ j ];

            if ( run_all || event.namespace_re.test( handleObj.namespace ) ) {

                event.handler = handleObj.handler;
                event.data = handleObj.data;
                event.handleObj = handleObj;

                var ret = handleObj.handler.apply( this, args );

                if ( ret !== undefined ) {
                    event.result = ret;
                    if ( ret === false ) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }

                if ( event.isImmediatePropagationStopped() ) {
                    break;
                }
            }
        }
        return event.result;
    },

    //...
}

This function simply goes through each handler function in the events object and executes each one in turn. First, the native browser event passed to jQuery.event.handle is passed into jQuery.event.fix to normalize the event properties across different browsers. A check is then performed to see if we're dealing with a namespaced event or an exclusive event (event handers that trigger only for the exact event i.e. no namespaces), and to use this information to filter the handle functions that will be executed accordingly. If a particular handler function returns a boolean result, then this is used to set the result on the event. A return value of false is shorthand for setting both event.preventDefault() and event.stopPropagation(), which are W3C event properties that are normalized to work the same across all browsers by the jQuery.event.fix method. We'll look at this function in the next part of the series.

Please leave a comment if this information has been useful :)


Comments

comments powered by Disqus