Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at Dan Wilson's 2011 (North Carolina) with:

My Fundamental Misunderstanding Of The jQuery Event Object

By Ben Nadel on

Typically, when I deal with event binding in jQuery, I use it in a very one-off kind of way. Meaning, I rarely have multiple event handlers bound to the same element and I rarely use event bubbling in which an event is utilized at more than one level of the DOM tree. As such, I never needed more than a superficial understanding of the jQuery event object. This morning, however, as I was further exploring custom event types in jQuery, it quickly became apparent that my understanding of the event object was, in reality, a critical misunderstanding.

 
 
 
 
 
 
 
 
 
 

To demonstrate where I was going wrong, I have put together this small example. In the following code, I am binding two event handlers to the same action ("click") on the same element ("p"). With each of these bindings, I am also defining additional event data which I plan to leverage in the event handler; the caveat being that I don't intend to make immediate use of the resultant event object:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery Event Object</title>
  • <script type="text/javascript" src="jquery-1.4a1.js"></script>
  • <script type="text/javascript">
  •  
  • jQuery(function( $ ){
  •  
  • // Bind click with extra event data.
  • $( "p" ).bind(
  • "click",
  • {
  • which: "first"
  • },
  • function( event ){
  • delayEvent( event );
  • }
  • );
  •  
  •  
  • // Bind click with extra event data.
  • $( "p" ).bind(
  • "click",
  • {
  • which: "second"
  • },
  • function( event ){
  • delayEvent( event );
  • }
  • );
  •  
  •  
  • // This is the same handler used by both event
  • // bindings above. Notice that it doesn't make use
  • // of the EVENT object right away, but rather delays
  • // its usage.
  • var delayEvent = function( event ){
  •  
  • // Output current event data.
  • console.log( "A:", event.data.which );
  •  
  • // In half a second, check the EVENT object to see
  • // what bound data is available.
  • setTimeout(
  • function(){
  • console.log( "B:", event.data.which );
  • },
  • 500
  • );
  •  
  • };
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <p>
  • Holy cow! Did you see Tricia today?!? HOT!!
  • </p>
  •  
  • </body>
  • </html>

As you can see above, both events define a unique value for the property, "which," which will be available in the event handler callback. Each event callback turns around and calls the same delayEvent() method, which itself, turns around and creates a timer. The passed-in event object is then only finally utilized within this timer callback, which outputs the event-data-scoped "which" value.

When we run the code above, we get the following console output:

A: first
A: second

B: second
B: second

As you can see, both calls to the delayEvent() method output the intended "which" value. However, once the subsequent timer executes (once per binding), both timer-delayed callbacks report the last-bound "which" value.

When I started to see this happen, I was working in a context that had closures inside of closures inside of closures; as such, I figured it was some sort of crazy variable-binding issue that I had never seen before. Once I moved the code into this white-page example, the problem became clear: I had a fundamental misunderstanding of how jQuery uses the resultant event objects across the various event binding callbacks.

My mis-assumption was that jQuery passed a fresh Event object to each event handler. What I realize now is that jQuery actually passes the same Event object around to every event handler that would be triggered by the given action. Because of this, the event object passed to the first event handler is subsequently populated with the second event handler's data by the time the first delayed-method gets executed. In hindsight, this makes perfect sense; after all, event methods like Event.isDefaultPrevented() and Event.isPropagationStopped() and Event.isImmediatePropagationStopped() almost only make sense if the same Event object is passed around to multiple event handlers.

With this clearer understanding in hand, I re-worked the above demo. This time, rather than depending on the delayed integrity of the given Event object, I am simply copying the required data out of the Event, to be lexically-bound to the setTimeout() callback:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery Event Object</title>
  • <script type="text/javascript" src="jquery-1.4a1.js"></script>
  • <script type="text/javascript">
  •  
  • jQuery(function( $ ){
  •  
  • // Bind click with extra event data.
  • $( "p" ).bind(
  • "click",
  • {
  • which: "first"
  • },
  • function( event ){
  • delayEvent( event );
  • }
  • );
  •  
  •  
  • // Bind click with extra event data.
  • $( "p" ).bind(
  • "click",
  • {
  • which: "second"
  • },
  • function( event ){
  • delayEvent( event );
  • }
  • );
  •  
  •  
  • // This is the same handler used by both event
  • // bindings above. Notice that it doesn't make use
  • // of the EVENT object right away, but rather delays
  • // its usage.
  • var delayEvent = function( event ){
  •  
  • // Because of the way jQuery uses the EVENT object
  • // in immediate and long-term propagation, we have
  • // to copy the event data out of the event object
  • // if we want to reference it later.
  • var data = event.data;
  •  
  • // Output current event data.
  • console.log( "A:", data.which );
  •  
  • // In half a second, check the copied DATA object
  • // to see what bound data is available.
  • setTimeout(
  • function(){
  • console.log( "B:", data.which );
  • },
  • 500
  • );
  •  
  • };
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <p>
  • Holy cow! Did you see Tricia today?!? HOT!!
  • </p>
  •  
  • </body>
  • </html>

Now that my primary event handler copies the event data out of the Event object and into the local function scope:

  • var data = event.data;

... my delayed callback references data that cannot be overridden asynchronously. And so, when we run the page this time, we get the following console output:

A: first
A: second

B: first
B: second

This time, it works like a charm. Now that I have a better understanding of how jQuery uses the generated event objects across multiple event handlers, I can get back to playing around with custom jQuery event types.




Reader Comments

So is what's actually happening that the browser instantiates the event object internally and jQuery gets handed that, which IT then feeds to all of the subscribing handlers by reference?

I wonder if jQuery has a setting or option to let you get the event object by value instead which seems like it would let you author more-easily-shareable code with less worrying about conflict with other jQuery code and inadvertent sabotage. Like adding your custom drag and drop animation effect to someone else's existing app, both of which might severely manipulate the mouse down event object. Coo post.

Reply to this Comment

PS I like that you're totally obsessed with jQuery, I love jQuery too.

I'd be interested in hearing your thoughts on the Flash Platform and Actionscript, and if you've thought about it, why you're as excited as you are about jQuery and AJAX apps without being even MORE excited about Actionscript and Flash/AIR-based web apps, which get me even hotter and bothereder than jQuery, especially given the silly great ColdFusion integration and performance.

Reply to this Comment

Scratch that. jQuery creates its own scratch object and actually copies over properties from the browser's original event object. That knowledge will come in handy some day if there's ever a Fallout 3 apocalypse and I find myself sitting around playing some kind of legacy technology trivia game in a vault.

Reply to this Comment

I am really liking these videos you are doing. It helps a ton to hear your voice talk as you mouse around the code.

I know the videos take a lot of extra time to produce, but they are making my job of consuming your content that much faster.

Reply to this Comment

Thanks Ben for figuring this out. As usual, your drive to go WAY beyond 'a superficial understanding' of the technologies you work with, taught me something new. You rock dude, keep exploring!

Reply to this Comment

@David,

I believe that jQuery attaches its own event listeners to whatever the target objects are (in a cross-browser compatible way, of course), and then, as you are saying, creates its own event object (new jQuery.Event()) which it then manually propagates.

As far as Flash and ActionScript, I actually do want to learn more about FLEX-type programming as I think it, like jQuery, is very much event-driven and would shed a lot of light on perhaps some "Best Practices" in event programming that I am not privy to.

Just not enough hours in the day ;)

@Dave,

I'm glad you like the videos - I'll keep making them. I think it helps to really paint the picture.

@Martijn,

Thank you.

@Ricardo,

Exactly! I've been using jQuery for a long time and only just ran into this issue for the first time. Granted, it took me over two hours to debug; but now that I see what's going on, I can act accordingly.

Reply to this Comment

Would this work as well?

// Bind click with extra event data.
$( "p" ).bind(
"click",
{which: "second"},
function( event ){
delayEvent( event, event.data);
}
);

var delayEvent = function( event, data){
// Output current event data.
console.log( "A:", data.which );
setTimeout(
function(){
console.log( "B:", data.which );
},
500
);
};

I just like the idea of splitting off the data as soon as possible on a potentially shared event object, never been to sure how stuff will actually execute, had weird circumstances where the sequence stuff executed was inconsistent, although that was with events at different levels instead of the same element, but it has made me paranoid about execution order or taking anything for granted with eventing.

Reply to this Comment

Nice post. This made me curious so I modified the original example to pass slightly different data in the 2 events.

jQuery(function( $ ){

// Bind click with extra event data.
$( "p" ).bind(
"click",
{
foo: "bar",
which: "first"
},
function( event ){
delayEvent( event );
}
);


// Bind click with extra event data.
$( "p" ).bind(
"click",
{
fizz:"buzz",
which: "second"
},
function( event ){
delayEvent( event );
}
);


// This is the same handler used by both event
// bindings above. Notice that it doesn't make use
// of the EVENT object right away, but rather delays
// its usage.
var delayEvent = function( event ){

// Output current event data.
console.log( "A:", event.data );

// In half a second, check the EVENT object to see
// what bound data is available.
setTimeout(
function(){
console.log( "B:", event.data );
},
500
);

};

});

Notice I'm logging the whole data object. The output is as follows.

A: Object foo=bar which=first
A: Object fizz=buzz which=second
B: Object fizz=buzz which=second
B: Object fizz=buzz which=second

So essentially the data property is entirely replaced with each event handler. This supports your findings. But does that make sense? It might be handier to do a merge on each subsequent event so you end up with something like this.

A: Object foo=bar which=first
A: Object foo=bar fizz=buzz which=second
B: Object foo=bar fizz=buzz which=second
B: Object foo=bar fizz=buzz which=second

That way you can treat the data object as state that is maintained between events. There are other solutions for that though so this is just academic.

Reply to this Comment

@Marco,

Good thinking. As I was looking at your code, I knew immediately where you were going with it. I think you are right - the entire data value is replaced. As to whether or not it makes sense to merge vs. replace, I am not sure.

The reason that merging might not make sense is that you can get overriding values. And, at that point - where we might lose data integrity for a given key - I think merging loses its usefulness.

Reply to this Comment

It could be a problem but it could also be useful. It depends on why you're stacking events and how you're using them.

Consider a scenario where you have a page in your app with the usual events. But you add some functionality that also needs to run if a user with certain roles is authenticated.

You could put checks in your original code to check user state. Or your new code could just add additional events that run. That code could override user state values to indicate some settings have changed.

This is definitely a contrived example and I'm not sure if I buy it myself :) But this bothers me less than actually losing data with the current implementation.

Reply to this Comment

@Marco,

I think it could work - you just have to be careful about how your events are wired together. After all, one of the nice things about event-driven architecture is that you don't have to worry about how many listeners are "listening" for events - they act independently.

Just something to be aware of in a pros / cons.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.