Les gestionnaires d’événement jQuery s’exécutent toujours dans l’ordre où ils étaient liés – de quelque manière que ce soit?

Il peut être fastidieux que les gestionnaires d’événements jQuery s’exécutent toujours dans l’ordre dans lequel ils ont été liés. Par exemple:

$('span').click(doStuff1); $('span').click(doStuff2); 

En cliquant sur la plage, doStuff1() se déclenche, suivi de doStuff2() .

Au moment où je lie doStuff2 (), je voudrais que l’option de le lier avant doStuff1 (), mais il ne semble pas y avoir de moyen facile de le faire.

Je suppose que la plupart des gens diraient, écrivez simplement le code comme ceci:

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

Mais ce n’est qu’un exemple simple: dans la pratique, il n’est pas toujours pratique de le faire.

Il existe des situations dans lesquelles vous souhaitez lier un événement et l’object auquel vous êtes lié contient déjà des événements. Et dans ce cas, vous pouvez simplement souhaiter que le nouvel événement se déclenche avant tout autre événement existant.

Alors, quel est le meilleur moyen d’y parvenir dans jQuery?

Réponse mise à jour

jQuery a modifié l’emplacement de stockage des événements dans 1.8. Maintenant vous savez pourquoi c’est une si mauvaise idée de jouer avec des API internes 🙂

La nouvelle API interne permettant d’accéder aux événements pour un object DOM est disponible via l’object jQuery global, et n’est pas liée à chaque instance. Elle prend un élément DOM comme premier paramètre et une clé (“events” pour nous) comme paramètre. deuxième paramètre.

 jQuery._data(, "events"); 

Alors, voici le code modifié pour 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); }); }; 

Et voici une aire de jeux .


Réponse originale

Comme @Sean l’a découvert, jQuery expose tous les gestionnaires d’événements via l’interface de data un élément. Spécifiquement element.data('events') . En utilisant cela, vous pouvez toujours écrire un plugin simple grâce auquel vous pouvez insérer n’importe quel gestionnaire d’événement à une position spécifique.

Voici un plugin simple qui ne fait que cela pour insérer un gestionnaire au début de la liste. Vous pouvez facilement étendre cela pour insérer un élément à n’importe quelle position. C’est juste une manipulation de tableau. Mais comme je n’ai pas vu le code source de jQuery et que je ne veux pas passer à côté de la magie jQuery, j’ajoute normalement le gestionnaire à l’aide de bind , puis redissortingbue le tableau.

 // [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); }; 

Ainsi, par exemple, pour ce balisage, cela fonctionnerait comme ( exemple ici ):

 
..
$("#me").click(function() { alert("1"); }); $("#me").click(function() { alert("2"); }); $("#me").bindFirst('click', function() { alert("3"); }); $("#me").click(); // alerts - 3, then 1, then 2

Cependant , étant donné que .data('events') ne fait pas partie de leur API publique, une mise à jour de jQuery pourrait casser votre code si la représentation sous-jacente des événements attachés passe d’un tableau à un autre, par exemple.

Clause de non-responsabilité: Comme tout est possible :), voici votre solution, mais je reviendrais quand même sur le refactoring de votre code existant, car il suffit d’essayer de se souvenir de l’ordre dans lequel ces éléments ont été connectés. de plus en plus de ces événements ordonnés.

Vous pouvez faire un espace de noms personnalisé des événements.

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

Ensuite, lorsque vous devez les déclencher, vous pouvez choisir l’ordre.

 $('span').sortinggger('click.doStuff1').sortinggger('click.doStuff2'); 

ou

 $('span').sortinggger('click.doStuff2').sortinggger('click.doStuff1'); 

De plus, il suffit de cliquer sur le bouton DEVRAIT déclencher à la fois dans l’ordre où ils ont été liés … pour que vous puissiez toujours le faire

 $('span').sortinggger('click'); 

Une très bonne question … J’étais insortinggué alors j’ai fait un peu de fouille; pour ceux qui sont intéressés, voici où je suis allé et ce que j’ai trouvé.

En regardant le code source de jQuery 1.4.2, j’ai vu ce bloc entre les lignes 2361 et 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; }; }); 

Il se passe beaucoup de choses intéressantes ici, mais ce qui nous intéresse, c'est entre les lignes 2384 et 2388:

 else { for ( var i = 0, l = this.length; i < l; i++ ) { jQuery.event.add( this[i], type, handler, data ); } } 

Chaque fois que nous appelons bind() ou one() nous faisons un appel à jQuery.event.add() ... alors regardons ça (lignes 1557 à 1672, si cela vous intéresse)

 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 sortinggger and when // an event is called after a page has unloaded return typeof jQuery !== "undefined" && !jQuery.event.sortingggered ? 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 sortingggering jQuery.event.global[ type ] = true; } // ... snip ... } 

À ce stade, j'ai réalisé que comprendre cela allait prendre plus de 30 minutes ... alors j'ai cherché Stackoverflow pour

 jquery get a list of all event handlers bound to an element 

et trouvé cette réponse pour itérer sur les événements liés:

 //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.toSsortingng() ); }); }); 

Tester cela dans Firefox Je vois que l'object events dans l'atsortingbut data de chaque élément a un atsortingbut [some_event_name] ( click dans notre cas) auquel est attaché un tableau d'objects handler , chacun ayant un guid, un namespace, un tapez, et un gestionnaire. "Donc," je pense, "nous devrions théoriquement pouvoir append des objects construits de la même manière au [element].data.events.[some_event_name].push([our_handler_object); ..."

Et puis je vais finir d'écrire mes découvertes ... et trouver une réponse bien meilleure postée par RusselUresti ... qui me présente quelque chose de nouveau que je ne connaissais pas à propos de jQuery (même si je le regardais droit dans les yeux .)

Ce qui prouve que Stackoverflow est le meilleur site de questions / réponses sur Internet, du moins à mon humble avis.

Donc, je poste ceci pour le bien de la postérité ... et le marque comme un wiki communautaire, puisque RussellUresti a déjà si bien répondu à la question.

Le principe standard est que les gestionnaires d’événements distincts ne doivent pas dépendre de l’ordre dans lequel ils sont appelés. S’ils dépendent de la commande, ils ne doivent pas être séparés.

Sinon, vous enregistrez un gestionnaire d’événement comme étant «premier» et quelqu’un d’autre enregistre alors son gestionnaire d’événement en tant que «premier» et vous êtes de retour dans le même désordre qu’auparavant.

.data (“events”) a été supprimé dans les versions 1.9 et 2.0beta, vous ne pouvez donc plus compter sur ces solutions.

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

Pour jQuery 1.9+, Dunstkreis a mentionné que .data (“events”) a été supprimé. Mais vous pouvez utiliser un autre hack (il n’est pas recommandé d’utiliser des possibilités non documentées). $ ._ data ($ (this) .get (0), ‘events’) à la place et la solution fournie par anurag ressemblera à ceci :

 $.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); }; 

La réponse sélectionnée créée par Anurag n’est que partiellement correcte. En raison de certains aspects internes de la gestion des événements de jQuery, la fonction bindFirst proposée ne fonctionnera pas si vous avez un mélange de gestionnaires avec et sans filtres (par exemple: $ (document) .on (“click”, handler) vs $ (document) .on (“clic”, “bouton”, gestionnaire).

Le problème est que jQuery placera (et attend) que les premiers éléments du tableau de gestionnaires seront ces gestionnaires filtrés, ainsi placer notre événement sans filtre au début rompt cette logique et les choses commencent à s’effondrer. La fonction bindFirst mise à jour doit être la suivante:

 $.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.first(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); }); }; 

Je suppose que vous parlez de l’aspect bouillonnant de l’événement. Il serait utile de voir également votre code HTML pour les éléments de span mentionnés. Je ne vois pas pourquoi vous voudriez changer le comportement de base comme celui-ci, je ne le trouve pas du tout ennuyant. Je suggère d’aller avec votre deuxième bloc de code:

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

Plus important encore, je pense que vous le trouverez plus organisé si vous gérez tous les événements pour un élément donné dans le même bloc que celui que vous avez illustré. Pouvez-vous expliquer pourquoi vous trouvez cela ennuyeux?

Voici une solution pour jQuery 1.4.x (malheureusement, la réponse acceptée n’a pas fonctionné pour 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; }; 

Les conseils de Chris Chilvers devraient être le premier plan d’action, mais parfois nous avons affaire à des bibliothèques tierces qui rendent la tâche difficile et nous obligent à faire des choses coquines … ce qui est le cas. IMO est un crime de présomption similaire à l’utilisation! Important en CSS.

Cela dit, en s’appuyant sur la réponse d’Anurag, voici quelques ajouts. Ces méthodes permettent plusieurs événements (par exemple “keydown keyup paste”), le positionnement arbitraire du gestionnaire et la réorganisation après le fait.

 $.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()); }); }); }; 

On pourrait extrapoler à ce sujet avec des méthodes qui placeraient un gestionnaire donné avant ou après un autre gestionnaire donné.