jQuery event handlers always execute in order they were bound - any way around this? [duplicate]
It can be anoying that jQuery event handlers always execute in the order they were bound. For example:
$('span').click(doStuff1);
$('span').click(doStuff2);
clicking on the span will cause doStuff1()
to fire, followed by doStuff2()
.
At the time I bind doStuff2(), I would like the option to bind it before doStuff1(), but there doesn't appear to be any easy way to do this.
I suppose most people would say, just write the code like this:
$('span').click(function (){
doStuff2();
doStuff1();
});
But this is just a simple example - in practise it is not always convienient to do that.
There are situations when you want to bind an event, and the object you are binding to already has events. And in this case you may simply want the new event to fire before any other existing events.
So what is the best way to achieve this in jQuery?
Solution 1:
Updated Answer
jQuery changed the location of where events are stored in 1.8. Now you know why it is such a bad idea to mess around with internal APIs :)
The new internal API to access to events for a DOM object is available through the global jQuery object, and not tied to each instance, and it takes a DOM element as the first parameter, and a key ("events" for us) as the second parameter.
jQuery._data(<DOM element>, "events");
So here's the modified code for jQuery 1.8.
// [name] is the name of the event "click", "mouseover", ..
// same as you'd pass it to bind()
// [fn] is the handler function
$.fn.bindFirst = function(name, fn) {
// bind as you normally would
// don't want to miss out on any jQuery magic
this.on(name, fn);
// Thanks to a comment by @Martin, adding support for
// namespaced events too.
this.each(function() {
var handlers = $._data(this, 'events')[name.split('.')[0]];
// take out the handler we just inserted from the end
var handler = handlers.pop();
// move it at the beginning
handlers.splice(0, 0, handler);
});
};
And here's a playground.
Original Answer
As @Sean has discovered, jQuery exposes all event handlers through an element's data
interface. Specifically element.data('events')
. Using this you could always write a simple plugin whereby you could insert any event handler at a specific position.
Here's a simple plugin that does just that to insert a handler at the beginning of the list. You can easily extend this to insert an item at any given position. It's just array manipulation. But since I haven't seen jQuery's source and don't want to miss out on any jQuery magic from happening, I normally add the handler using bind
first, and then reshuffle the array.
// [name] is the name of the event "click", "mouseover", ..
// same as you'd pass it to bind()
// [fn] is the handler function
$.fn.bindFirst = function(name, fn) {
// bind as you normally would
// don't want to miss out on any jQuery magic
this.bind(name, fn);
// Thanks to a comment by @Martin, adding support for
// namespaced events too.
var handlers = this.data('events')[name.split('.')[0]];
// take out the handler we just inserted from the end
var handler = handlers.pop();
// move it at the beginning
handlers.splice(0, 0, handler);
};
So for example, for this markup it would work as (example here):
<div id="me">..</div>
$("#me").click(function() { alert("1"); });
$("#me").click(function() { alert("2"); });
$("#me").bindFirst('click', function() { alert("3"); });
$("#me").click(); // alerts - 3, then 1, then 2
However, since .data('events')
is not part of their public API as far as I know, an update to jQuery could break your code if the underlying representation of attached events changes from an array to something else, for example.
Disclaimer: Since anything is possible :), here's your solution, but I would still err on the side of refactoring your existing code, as just trying to remember the order in which these items were attached can soon get out of hand as you keep adding more and more of these ordered events.
Solution 2:
You can do a custom namespace of events.
$('span').bind('click.doStuff1',function(){doStuff1();});
$('span').bind('click.doStuff2',function(){doStuff2();});
Then, when you need to trigger them you can choose the order.
$('span').trigger('click.doStuff1').trigger('click.doStuff2');
or
$('span').trigger('click.doStuff2').trigger('click.doStuff1');
Also, just triggering click SHOULD trigger both in the order they were bound... so you can still do
$('span').trigger('click');
Solution 3:
A very good question ... I was intrigued so I did a little digging; for those who are interested, here's where I went, and what I came up with.
Looking at the source code for jQuery 1.4.2 I saw this block between lines 2361 and 2392:
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 ) ) {
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;
};
});
There is a lot of interesting stuff going on here, but the part we are interested in is between lines 2384 and 2388:
else {
for ( var i = 0, l = this.length; i < l; i++ ) {
jQuery.event.add( this[i], type, handler, data );
}
}
Every time we call bind()
or one()
we are actually making a call to jQuery.event.add()
... so let's take a look at that (lines 1557 to 1672, if you are interested)
add: function( elem, types, handler, data ) {
// ... snip ...
var handleObjIn, handleObj;
if ( handler.handler ) {
handleObjIn = handler;
handler = handleObjIn.handler;
}
// ... snip ...
// Init the element's event structure
var elemData = jQuery.data( elem );
// ... snip ...
var events = elemData.events = elemData.events || {},
eventHandle = elemData.handle, eventHandle;
if ( !eventHandle ) {
elemData.handle = eventHandle = function() {
// Handle the second event of a trigger and when
// an event is called after a page has unloaded
return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
jQuery.event.handle.apply( eventHandle.elem, arguments ) :
undefined;
};
}
// ... snip ...
// Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn);
types = types.split(" ");
var type, i = 0, namespaces;
while ( (type = types[ i++ ]) ) {
handleObj = handleObjIn ?
jQuery.extend({}, handleObjIn) :
{ handler: handler, data: data };
// Namespaced event handlers
^
|
// There is is! Even marked with a nice handy comment so you couldn't miss it
// (Unless of course you are not looking for it ... as I wasn't)
if ( type.indexOf(".") > -1 ) {
namespaces = type.split(".");
type = namespaces.shift();
handleObj.namespace = namespaces.slice(0).sort().join(".");
} else {
namespaces = [];
handleObj.namespace = "";
}
handleObj.type = type;
handleObj.guid = handler.guid;
// Get the current list of functions bound to this event
var handlers = events[ type ],
special = jQuery.event.special[ type ] || {};
// Init the event handler queue
if ( !handlers ) {
handlers = events[ type ] = [];
// ... snip ...
}
// ... snip ...
// Add the function to the element's handler list
handlers.push( handleObj );
// Keep track of which events have been used, for global triggering
jQuery.event.global[ type ] = true;
}
// ... snip ...
}
At this point I realized that understanding this was going to take more than 30 minutes ... so I searched Stackoverflow for
jquery get a list of all event handlers bound to an element
and found this answer for iterating over bound events:
//log them to the console (firebug, ie8)
console.dir( $('#someElementId').data('events') );
//or iterate them
jQuery.each($('#someElementId').data('events'), function(i, event){
jQuery.each(event, function(i, handler){
console.log( handler.toString() );
});
});
Testing that in Firefox I see that the events
object in the data
attribute of every element has a [some_event_name]
attribute (click
in our case) to which is attatched an array of handler
objects, each of which has a guid, a namespace, a type, and a handler. "So", I think, "we should theoretically be able to add objects built in the same manner to the [element].data.events.[some_event_name].push([our_handler_object);
... "
And then I go to finish writing up my findings ... and find a much better answer posted by RusselUresti ... which introduces me to something new that I didn't know about jQuery (even though I was staring it right in the face.)
Which is proof that Stackoverflow is the best question-and-answer site on the internet, at least in my humble opinion.
So I'm posting this for posterity's sake ... and marking it a community wiki, since RussellUresti already answered the question so well.
Solution 4:
The standard principle is separate event handlers shouldn't depend upon the order they are called. If they do depend upon the order they should not be separate.
Otherwise, you register one event handler as being 'first' and someone else then registers their event handler as 'first' and you're back in the same mess as before.