jQuery event handlers always execute in order they were bound - any way around this? [duplicate]
Asked Answered
S

10

145

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?

Shaquana answered 2/3, 2010 at 3:6 Comment(5)
Are you just saying that when you bind the 2nd event handler you want it to by called before the 1st?Booher
Can you give an example when it is not convenient to use your second example?Alexandretta
@Russell: If I already have some code which has bound events to an element. In that case, I may not have the option to go back and refactor that code. I would find it convienient to just be able to push an event onto the beginning of the event stack for the element.Shaquana
I'd like to contribute, though a bit late: not only the order in which you bind the handlers matters, but also the level in the DOM hierarchy where the node you are binding handlers to is placed. F.e. suppose you have $('body').on('click', fn1) and $('body #any').on('click', fn2); if you do $('body #any').trigger('click'), fn2 will execute first regardless off bind order.Thistledown
Please note that even when here are answers to the question of how to do this, there is not much concern about why would you like to do it. In short: you shouldn't do it, is a bad design, as using !important in CSS is most times. See https://mcmap.net/q/76509/-jquery-event-handlers-always-execute-in-order-they-were-bound-any-way-around-this-duplicate.Nunuance
R
124

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.

Redstart answered 14/4, 2010 at 21:6 Comment(16)
You're right, if .data() is not part of the API it is a risk using it. However, I like your suggestion to encapsulate the use of it in a new jQuery function. At least then if .data() breaks in a later release, you only have to update one functionShaquana
good point.. this way the changes will be isolated to one function.Redstart
api.jquery.com/jQuery.dataIberia
From the docs: Calling jQuery.data(element) retrieves all of the element's associated values as a JavaScript object. Note that jQuery itself uses this method to store data for internal use, such as event handlers, so do not assume that it contains only data that your own code has stored.Redstart
To support namespaces, replace the line to fetch the handlers: var handlers = this.data('events')[name.split('.')[0]];Lita
thanks @Martin - added your suggestion.Redstart
Hi @Redstart one of the other comments suggests this method no longer works as of jQuery 1.6.4 (the current version today). Do you have time to review this solution?Byebye
Could you add an example on jsfiddle? I tried reproducing the problem but could not using jQuery 1.6.4 - jsfiddle.net/j6HP5/31Redstart
As of (at least) jQuery 1.8.0, this no longer works as this.data('events') is undefined. However this.data still seems to exist but I'm not sure if there's a new solution.Glaze
Hi @Glaze - It may be a little late now, but I've updated the answer for jQuery 1.8.Redstart
So where do I petition jQuery to have them add this as an official part of their API?Othilia
Sorry but this doesn't seem to work in all cases: my fiddle. Just start typing something in the result box and you'll see it shows a message that should not appear. Is this a bug or a problem with messing with the event queue?Alanis
Is there any way to make this work with submit - seems not to work with submit events.Aquarius
This does not works if an event is set directly on the DOM element (in the HTML). Just a little fix let this works with DOM events too, check here : jsfiddle.net/leeroy/ft1bddhk/2Relay
Is it possible to accommodate delegation in here?Aggy
Got it, I ended up using delegate in spite of bindAggy
D
37

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'); 
Delindadelineate answered 14/4, 2010 at 20:33 Comment(2)
+1 You learn something new every day. Thank you Russell!Brawley
Thanks mate. I didn't know about custom namespaces either. I'll definitly be able to use this technique in the future. I gave the correct answer to Anurag, because it was closer to what I was trying to achieve in this circumstance.Shaquana
B
13

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.

Brawley answered 2/3, 2010 at 3:7 Comment(1)
+1 Thanks for all of your analysis Sean. Very helpfulShaquana
P
5

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.

Prophetic answered 14/4, 2010 at 20:43 Comment(0)
E
5

The selected answer authored by Anurag is only partially correct. Due to some internals of jQuery's event handling, the proposed bindFirst function will not work if you have a mix of handlers with and without filters (ie: $(document).on("click", handler) vs $(document).on("click", "button", handler)).

The issue is that jQuery will place (and expect) that the first elements in the handler array will be these filtered handlers, so placing our event without a filter at the beginning breaks this logic and things start to fall apart. The updated bindFirst function should be as follows:

$.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();
        // get the index of the first handler without a selector
        var firstNonDelegate = handlers.find(function(h) { return !h.selector; });
        var index = firstNonDelegate ? handlers.indexOf(firstNonDelegate)
                                     : handlers.length; // Either all handlers are selectors or we have no handlers
        // move it at the beginning
        handlers.splice(index, 0, handler);
    });
};
Erose answered 16/9, 2014 at 18:12 Comment(2)
I was wondering about this! Thanks. Only problem is, this version does not accept an argument for delegated events itself. I would like to be able to call $.fn.bindFirst(name, selector, handler). I'll try to figure out how, but since you seem to understand this better than me, I'm sure the community would benefit from another revision.Opsonize
It should be very similar, though you'd have to do some argument shuffling before the call to this.on(name, fn);. Basically add in the parameter for selector in the signature, then immediately check if selector is a function or a string. If selector is a function, then set fn = selector and selector = null. Finally, within the this.each, you'd have to change the logic to insert at the first non-delegate if selector is null or at 0 if selector is not null.Erose
H
4

.data("events") has been removed in versions 1.9 and 2.0beta, so you cant any longer rely on those solutions.

http://jquery.com/upgrade-guide/1.9/#data-quot-events-quot-

Hasbeen answered 18/1, 2013 at 14:42 Comment(1)
The answer was update and here is a fiddle showing it work in jquery 2.edge jsfiddle.net/g30rg3/x8Na8/171Attack
C
3

For jQuery 1.9+ as Dunstkreis mentioned .data('events') was removed. But you can use another hack (it is not recommended to use undocumented possibilities) $._data($(this).get(0), 'events') instead and solution provided by anurag will look like:

$.fn.bindFirst = function(name, fn) {
    this.bind(name, fn);
    var handlers = $._data($(this).get(0), 'events')[name.split('.')[0]];
    var handler = handlers.pop();
    handlers.splice(0, 0, handler);
};
Clare answered 23/3, 2014 at 13:22 Comment(1)
I used delegate in spite of bind for event delegation.Aggy
A
3

Chris Chilvers' advice should be the first course of action but sometimes we're dealing with third party libraries that makes this challenging and requires us to do naughty things... which this is. IMO it's a crime of presumption similar to using !important in CSS.

Having said that, building on Anurag's answer, here are a few additions. These methods allow for multiple events (e.g. "keydown keyup paste"), arbitrary positioning of the handler and reordering after the fact.

$.fn.bindFirst = function (name, fn) {
    this.bindNth(name, fn, 0);
}

$.fn.bindNth(name, fn, index) {
    // Bind event normally.
    this.bind(name, fn);
    // Move to nth position.
    this.changeEventOrder(name, index);
};

$.fn.changeEventOrder = function (names, newIndex) {
    var that = this;
    // Allow for multiple events.
    $.each(names.split(' '), function (idx, name) {
        that.each(function () {
            var handlers = $._data(this, 'events')[name.split('.')[0]];
            // Validate requested position.
            newIndex = Math.min(newIndex, handlers.length - 1);
            handlers.splice(newIndex, 0, handlers.pop());
        });
    });
};

One could extrapolate on this with methods that would place a given handler before or after some other given handler.

Avruch answered 19/6, 2014 at 22:43 Comment(0)
U
1

I'm assuming you are talking about the event bubbling aspect of it. It would be helpful to see your HTML for the said span elements as well. I can't see why you'd want to change the core behavior like this, I don't find it at all annoying. I suggest going with your second block of code:

$('span').click(function (){
  doStuff2();
  doStuff1();
});

Most importantly I think you'll find it more organized if you manage all the events for a given element in the same block like you've illustrated. Can you explain why you find this annoying?

Utopia answered 2/3, 2010 at 3:13 Comment(1)
This was just an example to illustrate the issue. My actual case is more complicated, such that to rewrite the code in that form would take some work. In some situations, I could even be using a 3rd party library and may not want to modify it. I just think it would be more convienient to have the option of adding the event to either the beginning or the end of the event stack. If there is a way of doing this in jQuery, I would be interested in finding out.Shaquana
I
1

Here's a solution for jQuery 1.4.x (unfortunately, the accepted answer didn't work for jquery 1.4.1)

$.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 copy = {1: null};

    var last = 0, lastValue = null;
    $.each(handlers, function(name, value) {
        //console.log(name + ": " + value);
        var isNumber = !isNaN(name);
        if(isNumber) {last = name; lastValue = value;};

        var key = isNumber ? (parseInt(name) + 1) : name;
        copy[key] = value;
    });
    copy[1] = lastValue;
    this.data('events')[name.split('.')[0]] = copy;
};
Iberia answered 8/6, 2011 at 16:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.