2010-03-02 13 views
123

Puede ser molesto que los manejadores de eventos jQuery siempre ejecuten en el orden en que estaban vinculados. Por ejemplo:Los manejadores de eventos de jQuery siempre se ejecutan en orden para que estén vinculados, ¿se puede evitar esto?

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

hacer clic en el lapso hará que doStuff1() al fuego, seguido de doStuff2().

En el momento Ato doStuff2(), me gustaría la opción para obligar a éste antes doStuff1(), pero no parece haber ninguna manera fácil de hacer esto.

supongo que la mayoría de la gente diría, acaba de escribir el código como el siguiente:

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

Pero esto es sólo un ejemplo sencillo - en la práctica no siempre es conveniente hacer eso.

Hay situaciones en las que desea vincular un evento, y el objeto al que está vinculando ya tiene eventos. Y en este caso, es posible que simplemente desee que el nuevo evento se active antes que cualquier otro evento existente.

Entonces, ¿cuál es la mejor manera de lograr esto en jQuery?

+0

¿Está diciendo que cuando vincula el segundo controlador de eventos que desea invocar antes de la 1? –

+0

¿Puedes dar un ejemplo cuando no es conveniente usar tu segundo ejemplo? – Russell

+0

@jarret: Sí, exactamente – asgeo1

Respuesta

110

respuesta Actualizado

jQuery cambiado la ubicación de donde los eventos se almacenan en 1.8. Ahora ya sabes por qué es tan mala idea para perder el tiempo con las API internas :)

El nuevo interna API para acceder a los eventos para un objeto DOM está disponible a través del objeto global jQuery, y no ligada a cada instancia, y toma un elemento DOM como el primer parámetro, y una tecla ("eventos" para nosotros) como el segundo parámetro.

jQuery._data(<DOM element>, "events"); 

Aquí está el código modificado para 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); 
    }); 
}; 

Y aquí hay un playground.


respuesta original

Como @Sean ha descubierto, jQuery expone todos los controladores de eventos a través de la interfaz data de un elemento. Específicamente element.data('events'). Con esto, siempre puedes escribir un plugin simple mediante el cual puedas insertar cualquier controlador de eventos en una posición específica.

Aquí hay un plugin simple que simplemente hace eso para insertar un controlador al principio de la lista. Puede extender esto fácilmente para insertar un elemento en cualquier posición determinada. Es solo manipulación de matriz. Pero como no he visto la fuente de jQuery y no quiero perderme ninguna magia de jQuery, normalmente agrego el controlador usando bind primero, y luego reorganizo la matriz.

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

Así, por ejemplo, para este marcado que funcionaría como (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 

Sin embargo, ya .data('events') no es parte de su API pública por lo que yo sé, una actualización de jQuery podría romper su código si la representación subyacente de los eventos adjuntos cambia de una matriz a otra cosa, por ejemplo.

Descargo de responsabilidad: Como todo es posible :), esta es su solución, pero aún así me equivocaría al refaccionar su código existente, ya que solo estoy tratando de recordar el orden en que se adjuntaron estos artículos. mano mientras sigue agregando más y más de estos eventos ordenados.

+4

Tiene razón, si .data() no es parte de la API, corre el riesgo de usarla. Sin embargo, me gusta su sugerencia para encapsular el uso de la misma en una nueva función jQuery. Al menos entonces si .data() se rompe en una versión posterior, solo tiene que actualizar una función – asgeo1

+2

buen punto ... de esta manera los cambios estarán aislados en una función. – Anurag

+0

http://api.jquery.com/jQuery.data/ –

1

Supongo que está hablando del aspecto burbujeante del evento. Sería útil ver su HTML para dichos elementos span también. No puedo entender por qué querrías cambiar el comportamiento central de esta manera, no me resulta nada molesto. Yo te sugeriría ir con su segundo bloque de código:

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

más importante creo que lo encontrará más organizado si usted maneja todos los eventos para un elemento dado en el mismo bloque como que he ilustrado. ¿Puedes explicar por qué encuentras esto molesto?

+0

Esto fue solo un ejemplo para ilustrar el problema. Mi caso real es más complicado, de modo que reescribir el código en esa forma tomaría algún trabajo. En algunas situaciones, incluso podría estar usando una biblioteca de terceros y puede que no quiera modificarla. Creo que sería más conveniente tener la opción de agregar el evento al principio * o al final de la pila de eventos. Si hay una forma de hacerlo en jQuery, me interesaría averiguarlo. – asgeo1

33

Puede hacer un espacio de nombre personalizado de eventos.

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

Luego, cuando necesite activarlos, puede elegir el orden.

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

o

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

Además, sólo clic activación deben dar lugar tanto en el orden en que fueron obligados ... lo que aún puede hacer

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

+1 Aprende algo nuevo todos los días. Gracias Russell! –

+0

Gracias amigo. Yo tampoco sabía sobre espacios de nombres personalizados. Definitivamente podré usar esta técnica en el futuro. Di la respuesta correcta a Anurag, porque estaba más cerca de lo que estaba tratando de lograr en esta circunstancia. – asgeo1

4

El principio estándar es evento separado los manejadores no deben depender del orden en que se llaman. Si dependen del orden, no deberían estar separados.

De lo contrario, registra un controlador de eventos como "primero" y otra persona registra su controlador de eventos como "primero" y vuelve al mismo desorden que antes.

12

Una muy buena pregunta ... Estaba intrigado así que hice un poco de excavación; para aquellos que están interesados, aquí es donde fui, y lo que se me ocurrió.

En cuanto al código fuente de jQuery 1.4.2 Vi este bloque entre las líneas 2361 y 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; 
    }; 
}); 

Hay una mucho de cosas interesantes que hacer aquí, pero la parte que nos interesa es entre las líneas 2384 y 2388:

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

Cada vez que llamamos bind() o one() que en realidad está haciendo una llamada a jQuery.event.add() ... así que vamos a echar un vistazo a eso (líneas 1557 a 1672, si está interesado)

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

En este punto me di cuenta de que la comprensión de que esto iba a tomar más de 30 minutos ... así que busqué Stackoverflow para

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

y encontró this answer para iterar sobre los acontecimientos ligados:

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

    }); 

}); 

Probando que en Firefox veo que el objeto events en el atributo data de cada elemento tiene un atributo [some_event_name] (click en nuestro caso) al que se le ha agregado una matriz de handler objetos, cada uno de los cuales tiene un guid, un espacio de nombres, un tipo y un controlador. "Entonces", creo, "deberíamos teóricamente poder agregar objetos construidos de la misma manera al [element].data.events.[some_event_name].push([our_handler_object); ..."

Y luego voy a terminar de escribir mis hallazgos ... y encontrar un mucho mejor respuesta publicada por RusselUresti ... que me presenta algo nuevo que no sabía sobre jQuery (aunque lo estaba mirando directamente)

¿Cuál es la prueba de que Stackoverflow es la mejor pregunta- y el sitio de respuesta en Internet, al menos en mi humilde opinión.

Así que estoy publicando esto por el bien de la posteridad ... y marcándolo como una wiki comunitaria, ya que RussellUresti ya respondió la pregunta muy bien.

+1

+1 Gracias por todos sus análisis Sean. Muy útil – asgeo1

1

he aquí una solución para jQuery 1.4.x (por desgracia, la respuesta aceptada no funcionó para 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; 
}; 
3

Para jQuery 1.9+ como Dunstkreis .data mencionados ('Eventos'). Pero se puede utilizar otro truco (no se recomienda el uso de las posibilidades sin papeles) $ ._ datos ($ (this) .get (0), 'eventos') en lugar y la solución aportada por anurag se verá así:

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

Usé 'delegate' a pesar de' bind' para la delegación de eventos. –

1

El consejo de Chris Chilvers debería ser el primer curso de acción, pero a veces estamos lidiando con bibliotecas de terceros que hacen que esto sea desafiante y requiere que hagamos cosas malas ... lo cual es así. OMI es un crimen de presunción similar al uso! Importante en CSS.

Habiendo dicho eso, basándose en la respuesta de Anurag, aquí hay algunas adiciones. Estos métodos permiten múltiples eventos (por ejemplo, "keydown keyup paste"), posicionamiento arbitrario del controlador y reordenación después del hecho.

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

Uno podría extrapolar esto con métodos que colocarían un controlador dado antes o después de algún otro controlador dado.

3

La respuesta seleccionada por Anurag es parcialmente correcta. Debido a algunos aspectos internos del manejo de eventos de jQuery, la función bindFirst propuesta no funcionará si tiene una combinación de controladores con y sin filtros (es decir: $ (documento) .on ("clic", controlador) frente a $ (documento) .on ("clic", "botón", controlador)).

El problema es que jQuery colocará (y esperará) que los primeros elementos en el conjunto de controladores serán estos manejadores filtrados, por lo que ubicar nuestro evento sin un filtro al principio rompe esta lógica y las cosas comienzan a desmoronarse. La función bindFirst actualizada debe ser la siguiente:

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

¡Me estaba preguntando sobre esto! Gracias. El único problema es que esta versión no acepta un argumento para los eventos delegados. Me gustaría poder llamar $ .fn.bindFirst (name, selector, handler). Trataré de averiguar cómo, pero ya que parece entender esto mejor que yo, estoy seguro de que la comunidad se beneficiaría de otra revisión. – fronzee

+1

Debería ser muy similar, aunque tendrías que hacer algunos argumentos arrastrando los pies antes de la llamada a 'this.on (name, fn);'. Básicamente agregue el parámetro para el selector en la firma, luego verifique inmediatamente si el selector es una función o una cadena. Si el selector es una función, configure fn = selector y selector = null. Finalmente, dentro de this.each, tendría que cambiar la lógica para insertar en el primer no delegado si el selector es nulo o en 0 si el selector no es nulo. – dshapiro

Cuestiones relacionadas