Debugging jQuery with Chrome's Developer Tools
When I first started experimenting with JavaScript back in the late 90's, most people used it to dynamically write web pages — Javascript code often looked something like this:
<script>
var c = document.cookie.split('; ');
for (var i in c) {
if (c[i].startsWith("accessCount")) {
document.write("You have access this page " + (c[i].split('=')[1]) + " times");
}
}
</script>
(And yes, late 90's Javascript was actually that lame). Over time, server-side frameworks got
to be more powerful (and more secure) for dynamic page creation, and Javascript was pushed out
to the "fringes" of user interactivity. More and more Javascript was designed in event handlers
: code executed in response to a user action like a button click or a form submission. By
the early 21st century, more of the Javascript I came across was along the lines of:
<script>
function validateForm(f) {
if (f.name.value == "") {
alert("You must supply a name");
return false;
}
return true;
}
</script>
<form id="inputForm" method="post" onsubmit="return validateForm(this)">
(Yep, early 21st century Javascript was pretty lame, too).
One unifying characteristic that these early Javascript experiments had was that the Javascript
was tightly intertwined with the HTML. Buttons had onclick
, onmouseover
,
onmouseout
, etc. handlers whose code was either in the <head>
section of the HTML page if you were lucky, or included just before the code that used it if you
were less lucky. What ended up happening as a result was that code ended up being cut and pasted
and slightly modified into different contexts. As Javascript got to be more and more complex,
the desire for reusable components was greater, and Javascript implementations started supporting
more of what was called "unobtrusive" Javascript. An unobtrusive form handler would have been
implemented as:
<script>
window.onload = function() {
var f = document.getElementById("inputForm");
f.onsubmit = function(f) {
if (f.name.value == "") {
alert("You must supply a name");
return false;
}
return true;
}
}
</script>
You could make this even more dynamic (and introduce support for multiple event handlers per
element) with addEventListener
— although going down that
path introduced some browser incompatibilities, since browsers could never quite seem to agree on
what to call that function. While this format made the HTML cleaner and could, in theory, be
more easily reused (example 3 couldn't be, but you can probably see how this could easily be
extended into a more robust attribute-driven validation framework), it made debugging just a little
bit harder. Back in the early days, if a form handler was misbehaving, you could do a "View
source", find the form declaration, look for its onsubmit
attribute, and then search
for that function name in your code. With unobtrusive javascript, the onsubmit
attribute is always separated from the element declaration, so you would often have to go on a bit
of a hunt to find the function declaration. However, the handler function, once bound, was still
bound to the element itself; if you knew the name of the element, you could always open up the
console or, if you were stuck with a browser without one, always resort to something like:
javascript:alert(document.getElementById('inputForm').onsubmit)
on the URL bar.
With Chrome, you can even open up the debugging tools and go straight to the Event Listener
tab of any given element and see a list of every event handler registered with it.
The benefits of separating presentation from behavior were clear enough that the extra debugging effort was worth it. With the advent of jQuery, though, this got to be even more obfuscated and more difficult. Examples 4, 5 and 6 demonstrate the "old school", unobtrusive old school, and jQuery way to register the same button onclick handler:
<script>
function report(b) {
alert("You clicked the button");
}
</script>
<button id="b" class="report" onclick="report(this)">click</button>
<head>
<script>
window.onload = function() {
document.getElementById("b").onclick = report;
}
</script>
<head>
<script>
$(function() {
$('#b').click(report);
}
</script>
While jQuery went a long way toward making really responsive Javascript UIs a possibility, giving us accordions, drop-down menus, fade in/out, animations, custom themes and more without having to resort to Flash — all in a truly cross-platform way — it also took over event registration and hid it deep within the internals of the jQuery library itself. This is never as evident as when you find yourself debugging somebody else's misbehaving Javascript — if you click a button and it doesn't do what it's supposed to do (or just does nothing at all, as buggy Javascript is prone to do), how do you go about tracking down the handler that's associated with the element? As figure 1 shows, even Chrome's debugging tools don't offer much assistance in this regard; the click handler isn't the custom code that you associated with the element, but a jQuery function.
A bit of debugging through the associated jQuery code reveals that jQuery associates a new property
called events
with each object that has a jQuery callback registered with it. As
shown in figure 2, you can take advantage of this knowledge and click through the defined function
to inspect it, break on it, whatever you need to do. It's worth noting that events
is an object which can contain multiple event handlers — you're not limited to just one
event handler in jQuery.
Not so fast, though. If you take a closer look at figure 1, you may notice that this click
handler is associated with jQuery 1.0.4... which is a pretty old jQuery version (9 years as of
this writing). jQuery has gone through quite a few revisions and events
was an
internal, "behavior is undefined" implementation detail. As it turned out, other framework
implementers had similar ideas, and this property ended up conflicting with those frameworks.
As of jQuery 1.1, the property was renamed to $events
.
jQuery 1.2 changed the internals of event handling yet again, and integrated event
handling with its general data tracking mechanism. With jQuery 1.2, if you wanted to track down
the handlers associated with an element whose ID you knew, you could query:
$.data($('#b')[0], "events")
. This works whether the event was registered by ID
or not — in other words, if the registration code were $(".report").click(report)
,
$.data($('#b')[0], "events")
would still show you the handler list for the button.
jQuery developers thought that the $.data($('#b')[0], "events")
construct was a bit
awkward, though, so jQuery's developers added a simplified construct:
$('#b').data('events')
. This was just a shortcut to the longer form that jQuery 1.2
supported, but was obviously preferred.
This continued to work until jQuery 1.8, when they changed the (undocumented, mind you) internals
of jQuery data handling yet again and renamed data
to _data
, and
(inexplicably) removed the shortcut for that was introduced in jQuery 1.3. As of the
time of this writing, the highest released version of jQuery is jQuery 2.1.4, which continues to
store event handlers in $._data
.
TL;DR
jQuery version | find event handlers for element id |
---|---|
1.0.0-1.0.4 | $('#id').events |
1.1.0-1.1.4 | $('#id').$events |
1.2.0-1.2.6 | $.data($('#id')[0], "events") |
1.3.0-1.7.2 | $('#id').data("events") |
1.8.0+ | $._data($('#id')[0], "events") |