Event Internals in jQuery - Part One

Published on Thursday, 10 March 2011 by Russ Cam

I've been using jQuery for a number of years now and think it's an awesome JavaScript library for providing a rich client experience to web applications with little effort.  Whilst I advocate the use of jQuery to quickly and effectively build your client side solutions, I also think that it's important to understand what's going on under the hood, particularly as it can help to quickly pinpoint a bug in your own code (and on the rarest of occasions, a bug in the library code).

In a series of posts, I'm going to be peeking under the covers of the jQuery event system. I've never seen a comprehensive write-up looking into the internals of the event system before, so I thought that this might be a good topic to cover. This first post will provide a little background to browser based events and will look at the root event binding function in jQuery, bind().

Why have an event system?

You may be wondering, why does jQuery even need an event system at all? After all, modern browsers have the ability to bind event handlers to events raised in the DOM, don't they? Whilst this is true, the event model that each of the myriad of browsers uses to achieve this can be very different indeed. The two main event models are so different in fact that, if you're doing any cross browser client side JavaScript programming, you end up writing an abstraction yourself to smooth out these browser differences. The way that jQuery achieves this is really quite elegant, but first a brief history lesson.

DOM Level 0 events

In the beginning, browsers had basic support for DOM events in the W3C DOM level 0 events (part of the DOM level 1 specification). With this event model, event handlers can be bound in two ways:

Inline

<a href="#" onclick="alert('hello world!');return false;">
    Clicking me will show an alert dialog box with the message 'hello world!'
</a>

With event handlers inline, the browser creates an anonymous function with the function body set to the contents of the string assigned to an element's event attribute value. Ultimately this results in a call to window.eval() to evaluate the string and execute as JavaScript.

Setting the Attribute value

<a id="my-anchor" href="#"> Clicking me will show an alert dialog box with the message 'hello world!'</a>
<script type="text/javascript">
    var a = document.getElementById('my-anchor');
    a.onclick = function() { alert('hello world!'); return false; };
</script>

With an event attribute on the element set, the anonymous function needs to be bound to the element once it exists in the DOM.

The DOM level 0 events are still the most widely supported across browsers, however, they lack the ability to bind more than one event handler to an event without jumping through hoops (defining an event handler function that calls all of the event handler functions you want to bind to the event) and binding events inline does little to promote Unobtrusive JavaScript, which strives for separation of content and behaviour.

DOM level 2 events (and Internet Explorer's proprietary event model)

With the limitations of DOM level 0 events in mind, modern versions of Mozilla, Opera, Safari, Chrome and Konqueror browsers use the standard W3C DOM level 2 events for binding handlers to events raised in the DOM. Internet Explorer on the other hand, uses a proprietary event model with some fundamental differences which we'll touch on briefly in a moment (side note: Internet Explorer 9 is expected to support the W3C DOM level 3 events draft specification – this would be very good news indeed).

addEventListener and removeEventListener

With the DOM level 2 events, there are two functions for binding and unbinding event handlers to events, addEventListener and removeEventListener, respectively

<a id="my-anchor" href="#"> Clicking me will show an alert dialog box with the message 'hello world!'</a>
<script type="text/javascript">
    var a = document.getElementById('my-anchor');
    function handler(e) { /* do something */  }

    // to bind
    a.addEventListener('click', handler , false);

    //to unbind
    a.removeEventListener('click', handler, false);
</script>

I won't go into a whole load of detail around these as the Mozilla Developer Reference and W3C specifications are both comprehensive. Of particular note however is that the signature for binding an event handler takes a string argument for the event name, an event handler function to execute when the event is raised and a Boolean flag to indicate whether the capture phase of the event should be used.

See that e parameter in the function that gets bound as the handler? In this event model, an event object containing properties related to the event raised is passed as the first argument to each bound event handler. This is often useful as depending on the event, it can contain useful properties that can be used to perform logic inside of the event handler. For example, a keypress event causes an event object to be passed to an event handler that contains a charCode property which can be used to determine the key that was pressed.

attachEvent and detachEvent

Similarly to the w3c event model, the Internet Explorer model also has two functions for binding and unbinding event handlers to events, attachEvent and detachEvent, respectively

<a id="my-anchor" href="#"> Clicking me will show an alert dialog box with the message 'hello world!'</a>
<script type="text/javascript">
    var a = document.getElementById('my-anchor');
    function handler(e) { /* do something */ }

    // to bind
    a.attachEvent('onclick', handler );

    //to unbind
    a.detachEvent('onclick', handler);
</script>

The first major difference is that the function names used to perform the binding and unbinding are different in IE's model to that used in the DOM level 2 events model. Secondly, the string name of the DOM event to which to attach the handler includes the prefix 'on'. Thirdly, this model does not support using the capture phase of an event to execute a handler function, only the bubbling phase. Peter-Paul Koch (ppk) has a great write-up over at quirksmode on event order.

What is this inside the handler function?

One of the biggest differences between the W3C and IE models is what the this keyword (the function context) refers to inside the event handler function; in the W3C model, this is a reference to the element on which the event handler is bound but in IE, this refers to the global object (the window object in browsers). This can really trip you up (pun intended) if you're not paying attention!

Event object

Equally as important as this, event handler functions do not receive an event object as the first argument (the argument for the e parameter will always be undefined) in IE's model; in IE, the event object is populated on a property named event on the global object. Some of the properties that exist on this event object are different to the ones that exist on the W3C event object, that is, different property names may be used that have a value that purports to the same piece of event data in each case. As an example, the W3C event object contains a property, target, that contains a reference to the element that the event was originally raised on. This same piece of data on IE's event object is assigned to a property named srcElement. Other properties may purport to the same piece of event data across event models but be calculated differently in different browsers and some event data is implemented in some event models but not others! Enter jQuery.

jQuery's Event Model

jQuery has done the hard work in abstracting away as many browser differences as possible. There are a number of objects and utility functions involved in the model, starting with .bind()

jQuery.fn.bind() and friends

When you call .bind() (or one of the specific event handler functions such as .click(), .change(), .keypress(), etc. which internally will call .bind() generally), the event handler function that is passed in, along with each element in the matched set of the jQuery object on which bind is called and any additional data that may also be passed in to bind (and available inside of the handler function) are all used in a call to a utility function, jQuery.event.add.

the bind function looks like the following:

jQuery.each(["bind", "one"], function( i, name ) {
    jQuery.fn[ name ] = function( type, data, fn ) {
        // Handle object literals
        if ( typeof type === "object" ) {
            for ( var key in type ) {
                this[ name ](key, data, type[key], fn);
            }
            return this;
        }

        if ( jQuery.isFunction( data ) || data === false ) {
            fn = data;
            data = undefined;
        }

        var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
            jQuery( this ).unbind( event, handler );
            return fn.apply( this, arguments );
        }) : fn;

        if ( type === "unload" && name !== "one" ) {
            this.one( type, data, fn );

        } else {
            for ( var i = 0, l = this.length; i < l; i++ ) {
                jQuery.event.add( this[i], type, handler, data );
            }
        }

        return this;
    };
});

We can see here that an array of strings is iterated over using the $.each() function. There's a quick check to see if we've been handed an object literal as the first argument and if so, to go through all of the properties of that object and bind the property value in each case to the event identified by the property name.

Both bind() and one() are declared as properties on jQuery.fn; this is an alias for the jQuery.prototype object, although in very early versions of the library, this was not the case. Mike Koss has a great introduction to the prototype object in JavaScript in discussing Object Oriented Programming in JavaScript if you're interested in more detail.

Next, there's a check to see if we've been passed an object as an argument for the data parameter; if data is a function or false, then data is assigned to fn and it is assumed that this is the event handler function that is to be bound to the event.

Now comes an interesting part. Since the source code deals with setting up both bind and one functions, if the one function is being set up, the $.proxy() is used so that the event handler function can be executed when the event to which it is being registered is raised and then can be unbound, using the context of an anonymous function that is also passed to the $.proxy function. $.proxy() returns a function, which in this case is fn with the context of this (which is the element on which the event handler function is being bound) and is passed the arguments of the anonymous function in which it is scoped. This returned function is assigned to the variable handler. In the case of bind, the fn function is simply assigned to handler.

Following on, if the event is unload and hasn't been bound with one(), then one() is used to register the event handler so that the function is removed from the internal cache after execution (more on the internal $.cache in the next part).

Finally, all of the matched elements in the jQuery object are iterated over and a call to the internal jQuery.event.add function is made to actually perform the subscription of the handler to the event.

In the next part we'll look at jQuery.event and its functions that underpin the jQuery Event Model.


Comments

comments powered by Disqus