For some time I have been meaning to poke a little at how jQuery works. Line references and code samples are from the dev version of jQuery 1.5.1 -
http://code.jquery.com/jquery-1.5.1.js.
First up, the ready function (http://api.jquery.com/ready/).
<!DOCTYPE html>
<html>
<head>
<title>How the heck does jQuery work?</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.5.1.js" ></script>
<script type="text/javascript">
$(onStart); //short-hand for $(document).ready(onStart);
function onStart($) {
alert('hello');
}
</script>
</head>
<body>
</body>
</html>
The $(onStart) call requests that our function be run when the DOM is available; normally if we just called onStart here the DOM probably wouldn't be loaded and we thus be unable to do cool stuff like attach event handlers or manipulate our page here because none of the objects have been created yet. The stack at time of loading is very short: We are at the very bottom!
Somewhat later our onStart function will be run via callback from jQuery. Presumably jQuery runs the ready function(s) we submit in response to some event so it seems natural to wonder what the callstack looks like when our onStart function actually runs. It turns out we are called from a stack of jQuery functions, rooted on DOMContentLoaded():
The handler function for DOMContentLoaded is setup differently depending on what browser capabilities are available:
jQuery uses the presence of addEventListener as a signal that DOMContentLoaded event is available (line 1052 above). In the past it has been fairly normal to use addEventListener as a way to detect a Mozilla based browser (eg http://dean.edwards.name/weblog/2005/09/busted/). DOMContentLoaded is ideal for ready() as it fires after the DOM is loaded but before things like large images have necessarily downloaded (M$ demo of this: http://ie.microsoft.com/testdrive/HTML5/DOMContentLoaded/Default.html). The "load" event on the other hand may wait for resources to load. Unfortunately, IE8 and lower don't support addEventListener (or DOMContentLoaded) so the failover to attachEvent is required.
IE9 will support addEventListener (http://www.davidflanagan.com/2010/03/ie9-will-have-a.html) and DOMContentLoaded (http://blogs.msdn.com/b/ie/archive/2010/03/26/dom-level-3-events-support-in-ie9.aspx) ... Yay!
The code at line ~1050 (above) merely sets up a DOMContentLoaded function based on the capabilities that appear to be available; something still needs to actually call it. The callbacks are attached to events by bindReady, at line 429.
Multiple events are setup to callback (eg both DOMContentLoaded and load) to jQuery.ready to ensure that
something actually runs it; exactly which ones are setup varies based on the browser capabilities available. Taking this type of nonsense away from most developers and having it "just work" across browsers is a large part of why jQuery is awesome - it takes FOREVER to figure out how to make this stuff work reliably.
When DOMContentLoaded ultimately fires it calls jQuery.ready. Ready does some work to try to confirm it's really time to run, double-checks the document is actually ready (line 407, again shielding us from browser weirdness), then executes the list of functions that have been listed to run on ready via resolveWith:
resolveWith does some gymnastics to try to avoid running repeatedly, then runs each of our callbacks:
That was kind of cool but it is sort of hard to understand merged into all the other jQuery code. Perhaps what is called for is to build our own :) Let us suppose we wanted our library to focus on running functions when something happened. We want usage to be something like this:
//SAMPLE USE OF OUR LIBRARY
when.DOMLoaded(onDomLoad);
function onDomLoad() {
alert('hello');
}
For this to work we'll need to declare 'when' and expose DOMLoaded. Something basic should suffice:
//OUR LIBRARY
function When() {
this.DOMLoaded = function(userFn) {
}
}
var when = new When();
Spiffy! All we have to do now is implement it. Luckily jQuery already did so we can rip off much of the relevant part of their implementation (simplified somewhat to try to keep the example clear):
//OUR LIBRARY
function When() {
var domLoadedHandlers = [];
var handleReady = function() {
while (domLoadedHandlers.length > 0) {
domLoadedHandlers.shift()();
}
}
// Mozilla, Opera and webkit nightlies currently support this event
if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", handleReady, false );
// A fallback to window.onload, that will always work
window.addEventListener( "load", handleReady, false );
} //else IE event model is used ... etc
this.DOMLoaded = function(userFn) {
domLoadedHandlers.push(userFn);
}
}
var when = new When();
//SAMPLE USE OF OUR LIBRARY
when.DOMLoaded(onDomLoad);
The implementation above has plenty of limitations: It only works on some browsers, it doesn't handle submission of a ready function after the loaded event has fired, errors from a ready function abort all processing, and so on.
At this point we can start to really appreciate the hard work jQuery is doing for us. Even seemingly trivial scripts - do something when the page is ready - are fairly ugly, with excitingly frequent browser-specific corner cases and hacks (IE-specific ready detection with special casing based on whether or not we are in a frame...). To make matters worse, just when you think you have it all correct a new version of the browser comes out and muddles everything up again. Thank god for jQuery!