I gestori di eventi jQuery vengono sempre eseguiti nell’ordine in cui erano vincolati, in ogni modo?

Può essere ansioso che i gestori di eventi jQuery eseguano sempre nell’ordine in cui erano vincolati. Per esempio:

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

facendo clic sullo span farà doStuff1() , seguito da doStuff2() .

Nel momento in cui lego doStuff2 (), vorrei l’opzione di collegarlo prima di doStuff1 (), ma non sembra esserci un modo semplice per farlo.

Suppongo che la maggior parte delle persone direbbe, basta scrivere il codice in questo modo:

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

Ma questo è solo un semplice esempio: in pratica non è sempre comodo farlo.

Esistono situazioni in cui si desidera associare un evento e l’object a cui si sta già vincolando ha già degli eventi. E in questo caso potresti semplicemente volere che il nuovo evento esploda prima di qualsiasi altro evento esistente.

Quindi qual è il modo migliore per ottenere questo in jQuery?

Risposta aggiornata

jQuery ha cambiato la posizione in cui sono archiviati gli eventi in 1.8. Ora sai perché è una pessima idea gironzolare con le API interne 🙂

La nuova API interna per accedere agli eventi per un object DOM è disponibile tramite l’object globale jQuery e non è legata a ciascuna istanza e accetta un elemento DOM come primo parametro e una chiave (“eventi” per noi) come secondo parametro.

 jQuery._data(, "events"); 

Quindi, ecco il codice modificato per 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); }); }; 

Ed ecco un parco giochi .


Risposta originale

Come @Sean ha scoperto, jQuery espone tutti i gestori di eventi attraverso l’interfaccia data un elemento. Specificamente element.data('events') . Usando questo si può sempre scrivere un semplice plugin per cui è ansible inserire qualsiasi gestore di eventi in una posizione specifica.

Ecco un semplice plugin che fa proprio questo per inserire un gestore all’inizio della lista. Puoi facilmente estenderlo per inserire un object in una data posizione. È solo manipolazione di schiere. Ma dato che non ho visto la fonte di jQuery e non voglio perdere nessuna magia jQuery, normalmente aggiungo il gestore usando prima bind e poi rimescola l’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); }; 

Ad esempio, per questo markup funzionerebbe come ( esempio qui ):

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

Tuttavia , dato che .data('events') non fa parte della loro API pubblica, per quanto ne so, un aggiornamento a jQuery potrebbe infrangere il tuo codice se la rappresentazione sottostante degli eventi collegati cambia da una matrice a qualcos’altro, per esempio.

Dichiarazione di non responsabilità: poiché tutto è ansible :), ecco la soluzione, ma continuerei a sbagliare sul lato del refactoring del codice esistente, poiché il semplice tentativo di ricordare l’ordine in cui questi elementi sono stati allegati può presto sfuggire di mano mentre continui ad aggiungere sempre più di questi eventi ordinati.

Puoi creare uno spazio dei nomi personalizzato per gli eventi.

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

Quindi, quando è necessario triggersrli, è ansible scegliere l’ordine.

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

o

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

Inoltre, basta fare clic sul trigger DOVREBBE sia nell’ordine in cui erano vincolati … quindi puoi ancora farlo

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

Una bella domanda … mi ha incuriosito e ho fatto un piccolo lavoro di scavo; per coloro che sono interessati, ecco dove sono andato, e cosa mi è venuto in mente.

Osservando il codice sorgente di jQuery 1.4.2 ho visto questo blocco tra le righe 2361 e 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; }; }); 

C'è un sacco di cose interessanti in corso qui, ma la parte che ci interessa è tra le righe 2384 e 2388:

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

Ogni volta che chiamiamo bind() o one() stiamo effettivamente effettuando una chiamata a jQuery.event.add() ... quindi diamo un'occhiata a questo (righe da 1557 a 1672, se sei interessato)

 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 ... } 

A questo punto mi sono reso conto che la comprensione di ciò avrebbe richiesto più di 30 minuti ... quindi ho cercato StackOverflow per

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

e ho trovato questa risposta per iterare gli eventi associati:

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

Provando che in Firefox vedo che l'object events nell'attributo data di ogni elemento ha un attributo [some_event_name] ( click nel nostro caso) al quale viene applicata una matrice di oggetti handler , ognuno dei quali ha un guid, un namespace, un scrivi e un gestore. "Quindi", penso, "dovremmo teoricamente essere in grado di aggiungere oggetti costruiti nello stesso modo per [element].data.events.[some_event_name].push([our_handler_object); ..."

E poi vado a finire di scrivere le mie scoperte ... e trovare una risposta molto migliore postata da RusselUresti ... che mi introduce a qualcosa di nuovo che non sapevo di jQuery (anche se lo stavo guardando dritto in faccia .)

Quale è la prova che StackOverflow è il miglior sito di domande e risposte su Internet, almeno a mio modesto parere.

Quindi sto postando questo per amore dei posteri ... e contrassegnandolo come una wiki della comunità, dal momento che RussellUresti ha già risposto alla domanda così bene.

Il principio standard è che i gestori di eventi separati non dovrebbero dipendere dall’ordine in cui sono chiamati. Se dipendono dall’ordine, non dovrebbero essere separati.

Altrimenti, si registra un gestore di eventi come “primo” e qualcun altro quindi registra il proprio gestore di eventi come “primo” e si torna nello stesso pasticcio di prima.

.data (“eventi”) è stato rimosso nelle versioni 1.9 e 2.0beta, quindi non puoi più affidarti a tali soluzioni.

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

Per jQuery 1.9+ come indicato da Dunstkreis, è stato rimosso .data (‘events’). Ma puoi usare un altro hack (non è raccomandato usare le possibilità non documentate) $ ._ dati ($ (this) .get (0), ‘eventi’) invece e la soluzione fornita da anurag sarà simile a:

 $.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 risposta selezionata, creata da Anurag, è solo parzialmente corretta. A causa di alcuni interni della gestione degli eventi di jQuery, la funzione bindFirst proposta non funzionerà se si dispone di un mix di gestori con e senza filtri (ad esempio: $ (documento) .on (“clic”, gestore) vs $ (documento). (“clic”, “pulsante”, gestore)).

Il problema è che jQuery posizionerà (e si aspetterà) che i primi elementi nell’array del gestore saranno questi gestori filtrati, quindi posizionare il nostro evento senza un filtro all’inizio infrange questa logica e le cose iniziano a cadere a pezzi. La funzione bindFirst aggiornata dovrebbe essere la seguente:

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

Suppongo che tu stia parlando dell’aspetto ribollente dell’evento. Sarebbe utile vedere il tuo codice HTML anche per gli elementi di span detti. Non vedo perché vorresti cambiare il comportamento di base come questo, non lo trovo affatto fastidioso. Suggerisco di andare con il tuo secondo blocco di codice:

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

Soprattutto, penso che lo troverai più organizzato se gestisci tutti gli eventi per un determinato elemento nello stesso blocco come hai illustrato. Puoi spiegare perché trovi questo fastidioso?

Ecco una soluzione per jQuery 1.4.x (sfortunatamente, la risposta accettata non ha funzionato con 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; }; 

Il consiglio di Chris Chilvers dovrebbe essere la prima linea d’azione, ma a volte abbiamo a che fare con librerie di terze parti che rendono questa sfida e ci impone di fare cose cattive … che è questo. IMO è un crimine di presunzione simile all’utilizzo! Importante nei CSS.

Detto questo, basandoci sulla risposta di Anurag, ecco alcune aggiunte. Questi metodi consentono più eventi (ad es. “Keydown keyup paste”), posizionamento arbitrario del gestore e riordino dopo il fatto.

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

Si potrebbe estrapolare su questo con metodi che posizionerebbero un dato gestore prima o dopo un altro gestore dato.