Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Heather Harkins
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Heather Harkins

Tracing Event Binding And Event Triggering In jQuery

By on

The other day, I had four developers (including myself) on a conference call, all trying to figure out why a particular state of our application was becoming unresponsive to mouse clicks. We were looking at a page that relied on thousands of lines of JavaScript, written over two years, by a half-dozen developers. Clearly, we were having an event binding issue; but, there was so much code in place, that it was difficult to tell what code may be precipitating such unexpected side-effects.

Ironically, it actually turned out to be a CSS issue (with Z-index), not an event issue; but, the whole exercise got me thinking - is there a way to trace the event binding and event triggering in jQuery? I know you can look at the data("events") property to see the current event bindings on a given DOM (Document Object Model) node; but, can we see something in more "real time"?

This morning, I sat down to see if I could, perhaps, override the jQuery on() method as a way to inject logging; and, while it may not be pretty, I think it may actually help me in a pinch.

To experiment with this, I created a small demo page that "accidentally" overrides a button click. I'm also including the jQuery UI library, as well, since it has a load of event bindings.

Test.htm - Our Demo Page

<!doctype html>
<html>
<head>
	<title>Tracing Events In jQuery</title>

	<!-- Include the jQuery UI classes. -->
	<link type="text/css" rel="stylesheet" href="./css/ui-lightness/jquery-ui-1.8.21.custom.css"></link>

	<!-- Include the jQuery library. -->
	<script type="text/javascript" src="../jquery-1.7.2.js"></script>

	<!--
		Include the TRACE library before any other jQuery-related
		libraries so that we have a chance to intercept event bindings.
	-->
	<script type="text/javascript" src="./jquery.event-trace.js"></script>
	<script type="text/javascript" src="jquery-ui-1.8.21.custom.min.js"></script>
	<script type="text/javascript">

		// When the DOM has loaded, initialize scripts.
		jQuery(function( $ ){


			// Bind to the BUTTON click.
			$( document ).on(
				"click",
				"form > button",
				function( event ){

					console.log( "Button clicked!" );

				}
			);


			// Bind the MODAL link.
			$( "a.modal" ).click(
				function( event ){

					$( "div.modal" ).dialog({
						modal: true
					});

				}
			);


			// Bind to the BUTTON click.
			$( "button" ).click(
				function( event ){

					return( false );

				}
			);


		});

	</script>
</head>
<body>


	<h1>
		Tracing Events In jQuery
	</h1>

	<form>
		<button type="button">Trigger Click!</button>
	</form>


	<p>
		<a href="#" class="modal">Open Modal</a>
	</p>

	<div class="modal" style="display: none ;">
		Hello!
	</div>


</body>
</html>

As you can see, we have one button click handler being delegated to the Document object. We also have another button click handler being bound directly to the button. Due to the way that delegation works, the second binding will actually cancel the first binding with its return(false) statement. Luckily, my jQuery event tracing plugin helps me see this in real time:

jquery.event-trace.js - Our Event Trace Plugin

// Create a private scope.
(function( $, on ){


	// I proxy the given function and return the resultant GUID
	// value that jQuery has attached to the function.
	var createAndReturnProxyID = function( target ){

		// When generating the proxy, it doesn't matter what the
		// "context" is (window in this case); we never actually
		// use the proxy - we just need the GUID that is tied to
		// it.
		var proxy = $.proxy( target, window );

		return( proxy.guid );

	};


	// Given an arguments collection (as the first argument), I return
	// the first value that is a function.
	var getFirstFunctionInstance = function( args ){

		for (var i = 0 ; i < args.length ; i++){

			// If this is a function, return it.
			if ($.isFunction( args[ i ])){

				return( args[ i ] );

			}

		}

	};


	// I print a horizontal rule to the console.
	var printHRule = function(){

		console.log( ". . . . . ." );

	};


	// I attempt to print the current stack trace.
	var printStackTrace = function(){

		// In order to gain access to the stack trace, let's throw
		// and catch an error object.
		try {

			throw new Error( "printStackTraceError" );

		} catch (error){

			// Get the raw trace (cross-browser).
			var rawTrace = (error.stack || error.stacktrace || "");

			// Break the raw trace into individual lines for printing.
			var stackTrace = rawTrace.split( /\n/g );

			// When printing the stack trace, we are going to start
			// on index 3. This allows us to skip the common parts:
			// 1 - *this* function.
			// 2 - the on() wrapper.
			// 3 - the core on() method.
			for (var i = 3 ; i < stackTrace.length ; i ++){

				if (stackTrace[ i ].length){

					console.log( " > " + stackTrace[ i ] );

				}

			}

		}

	};


	// I use a timer to debounce hrule requests. This way, several
	// successive calls to the function only result in one HRule
	// printing.
	var showHRuleAfterPause = function(){

		clearTimeout( hruleTimer );

		// Print the hrule if the timer is allowed to run its course.
		hruleTimer = setTimeout( printHRule, hruleTimerDuration );

	};


	// We are going to be overriding the core on() method in the
	// jQuery library. But, we do want to have access to the core
	// version of the on() method for binding. Let's get a reference
	// to it for later use.
	var coreOnMethod = $.fn.on;

	// To help keep track of the bind-trigger relationship, we are
	// going to assign a unique ID to each bind instance.
	var bindCount = 0;

	// I enable the debouncing of the hrule print.
	var hruleTimer = null;
	var hruleTimerDuration = (.5 * 1000);


	// Override the core on() method so that we can inject logging
	// around the binding / trigger of the event handlers.
	$.fn.on = function( types, selector, data, fn, /*INTERNAL*/ one ){

		// Get the unique bind index for this event handler. We can
		// use this index value to connect the bind to the subsequent
		// trigger events.
		var bindIndex = ++bindCount;

		// Print the general bind() properties.
		console.log(
			("Bind[ " + bindIndex + " ][ " + types + " ]:"),
			(selector || "*no-selector*"),
			this
		);

		// Print the stack trace at the time of the binding (so that
		// we can more easily track down where bindings are coming
		// from).
		printStackTrace();

		// the on() method accepts different argument schemes; as
		// such, the fn arguemnt may NOT be the actual function. Let's
		// just grab the first Function instance.
		var fnArgument = getFirstFunctionInstance( arguments );

		// Wrap the incoming event handler so that we can log
		// information surrounding its use.
		var fnWrapper = function( event ){

			// Log the event handler usage and event.
			console.log(
				("Trigger[ " + bindIndex + " ]:"),
				event.type,
				event
			);

			// Print the HRULE (so the console is easer to read).
			showHRuleAfterPause();

			// Execute the underlying event handler.
			return(
				fnArgument.apply( this, arguments )
			);

		};

		// Tie the wrapper and the underlying event handler together
		// using jQuery's proxy() functionality - this way, the events
		// can be property unbind() with the wrapper in place.
		fnWrapper.guid = createAndReturnProxyID( fnArgument );

		// Bind the wrapper as the event handler using the core,
		// underlying on() method.
		return(
			coreOnMethod.call( this, types, selector, data, fnWrapper, one )
		);

	};


})( jQuery );

This trace plugin works by wrapping the native on() method to inject logging statements around the event binding and the event triggering within the jQuery event model. This plugin uses some "private" information about the jQuery implementation; so, clearly it's subject to change at any time. Of course, you're not going to be using anything like this in production - it's just for debugging.

Anyway, thought this might be useful down the road.

Want to use code from this post? Check out the license.

Reader Comments

198 Comments

On a related note, modern browsers have really great developer tools nowadays that can help really go a long way to help troubleshoot these kinds of problems. The problem is some of the tools are a little more obscure and don't get much press coverage.

For example, in Firefox you can see a 3D view of your elements by z-index by pressing [CTRL][SHIFT][I] and clicking on the "3D View" button (in the bottom toolbar.) This feature has helped me a few times figure out z-index issues where elements I thought were on top, we not. ;)

Chrome (and Firefox's Firebug) can let you put stop break points on elements when changes to a DOM node occur--which can also really help with this kind of stuff.

15,640 Comments

@Judson,

Thanks my man!

@Dan,

Breakpoints is definitely something I need to learn more about. I'm so used to a CFDump/CFAbort workflow that setting external breakpoints never really clicked with me.

I can't get the Z-index thing to work - I'm not sure which "3D view" button you're talking about. Sounds beastly, though!

198 Comments

@Duncan:

The "Tilt" add-on is no longer needed--it's built into Firefox 11+ and was the feature I was talking about.

@Ben:

To get to the the 3D view, you must open the native Firefox Inspector (right click on a page, and press "Q" when the context menu appears.) From there you should be able to press ALT+W to enable the 3D view.

See these links for information on using the 3D Dom view:

https://developer.mozilla.org/en/Tools/Inspector

https://developer.mozilla.org/en/Tools/Page_Inspector/3D_view

15,640 Comments

@Dan,

Dang!!!! That's a crazy view of the page :D

@Creage,

Ah, good point, I completely forgot that you could bind more than one type of handler at a time. While I may pass multiple, space-delimited event types, I generally only ever pass in one handler per binding. Totally forgot that you even have the option to do other things.

15,640 Comments

@Luke,

Dang, that's a really awesome set of bookmarks! I love the way it even uses little icons for the various event types. Very cool!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel