Porqué no debemos usar nunca jQuery Live

12 May 2011

Introducción

Hace ya algún tiempo que preparé el primer borrador para este artículo pero a raíz de uno similar escrito por Justin Meyer decidí no publicarlo. Sin embargo, al igual que pasó con el anterior «jQuery: el mal uso de return false«, creo que el tema es lo suficientemente interesante como para compartir su contenido con la comunidad hispana y completar sobre la marcha algunos de los ejemplos y explicaciones que aparecen en el original. Así que ahí vamos: analizaremos en esta ocasión porqué no deberíamo usar tan a la ligera el socorrido método .live() de jQuery.

Eventos en jQuery

Uno de los aspectos más interesantes durante el uso de una biblioteca como jQuery o Mootools es su capacidad para manejar eventos de un modo transparente y válido en un entorno multinavegador. Por lo general, una asociación simple tendría el siguiente aspecto:

$('#myDomElement').click( function(){
  alert( 'Click!' );
});

Gracias al uso de un callback, podemos asociar una determinada acción/comportamiento a un evento determinado; en este caso, un click sobre un elemento del DOM.

Este tipo de estructuras la solemos aprendemos el primer día jugando con que nos acercamos a este tipo de bibliotecas. Sin embargo, tienen un defecto: solo funcionan sobre aquellos elementos que están presentes en el DOM tras la invocación del método document ready. Esto quiere decir que, una vez iniciado el script, si añadimos de forma dinámica un nuevo elemento, éste no responderá a los eventos que hayamos definidos anteriormente. Un caso típico sería el siguiente:

$(document).ready( function(){
  $('.myListElement').click( function(){
    alert(' Click! ');
  });
 
  $('.addNewListElement').click( function(){
    $('<li class="myListElement" />').appendTo('#myList');
  });
} );

Tenemos el mismo evento anterior asociado a un li en nuestra página. Además, hemos incluido un botón que añade nuevos elementos li cuando se pulsa. Si hacemos click sobre algún elemento de la lista, obtenemos el alert indicado en el primer callback pero, si añadimos dinámicamente uno nuevo y pulsamos sobre él, no obtendremos respuesta alguna: no se han delegado los eventos desde el original a las nuevas copias.

Casi igual de rápido que aprendemos a asociar eventos, descubrimos que existe una forma rápida de delegación: el método live().

Delegando eventos con live

El ejemplo anterior funcionaría como se espera si cambiamos el método click por el socorrido live:

$(document).ready( function(){
  $('.myListElement').live( 'click', function(){
    alert(' Click! ');
  });
 
  $('.addNewListElement').click( function(){
    $('<li class="myListElement" />').appendTo('#myList');
  });
} );

Ahora todo parece ir bien: si añadimos nuevos elementos pulsando nuestro botón, éstos heredan los eventos que hayamos definido anteriormente. Si pulsamos sobre cualquiera de ellos, obtenemos nuestro alert.

Esta solución es la que aparece siempre en los foros y tutoriales en Internet por lo que, una vez comprobado que funciona, no solemos profundizar más en cómo lo hace y qué implica. Rara vez continuamos leyendo hasta llegar a otro método mucho menos conocido pero con una funcionalidad similar: delegate

Delegando eventos con delegate

Pese a que también está diseñado para delegar eventos, la sintaxis de delegate es ligeramente diferente a la de live. El ejemplo anterior se escribiría como sigue:

$(document).ready( function(){
  $('#myList').delegate( '.myListElement', 'click', function(){
    alert(' Click! ');
  });
 
  $('.addNewListElement').click( function(){
    $('<li class="myListElement" />').appendTo('#myList');
  });
} );

Como podemos ver, recibe tres parámetros: el elemento dentro del grupo (padre) seleccionado, el evento a asociar y su correspondiente callback.

De nuevo, funciona como se espera: pulsamos sobre algún elemento añadido en tiempo de ejecución y responde correctamente. Sin embargo, hay una importante diferencia por debajo.

Diferencia entre live y delegate

La diferencia crítica entre ambos métodos es que, mientras que live asocia el evento al nivel más alto del DOM (el document), delegate se limita únicamente a aquellos que son llamados explícitamente en su selector.

Esto quiere decir que en el ejemplo con delegate, cuando un usuario hace click sobre un elemento li, este evento sube hasta el padre (que es realmente el objeto jQuery seleccionado) y dispara el manejador click. Entonces, se comprueba si efectivamente cumple con el criterio del selector («.myListElement») y, en caso de coincidir, ejecuta el correspondiente callback.

En el ejemplo con live, jQuery asocia el manejador al elemento más alto dentro de la estructura DOM, el document. Cualquier click en la página subirá hasta ahí y disparará el manejador que hemos definido. Se comprobará entonces si alguno de los elementos por encima del seleccionado (o el mismo) coinciden con el criterio definido (‘.myListElement’). Asumiendo que el usuario ha hecho click sobre un li o cualquier otra cosa en su interior, algún punto de la cadena desde ahí hacia el document coincidirá con la selección y ejecutará el callback.

A priori, podemos comprobar como el esfuerzo que realiza jQuery para monitorizar los eventos es mucho mayor con live que con delegate. Sin embargo, al margen del rendimiento, existen otras consideraciones a tener en cuenta.

No debes usar live para widgets que pueden aparecer más de una vez en una página

Aquí tenemos un ejemplo muy común. Pensemos en un widget compuesto por una capa con pestañas. Queremos que, cuando se pulse en una de esas pestañas, ocurra una acción. Como live asocia el evento al document, si incluimos dos de estos widgets en la misma página, cada vez que pinchemos en una pestaña el manejador de ambas será invocado. Esto quiere decir que si pincho sobre una pestaña en el primer widget, se invocará también el manejador del segundo. Lo ideal es sólo escuchemos el evento del elemento sobre el que hemos pinchado, no todos los coincidentes!.

 <div class="tab1 tabwidget">
    <div class="tab"> ... </tab>
    <div class="tab> ... </tab>
  </div>
  <div class="tab2 tabwidget">
    <div class="tab"> ... </tab>
    <div class="tab> ... </tab>
  </div>

Live puede decirse que crea manejadores globales mientras que delegate solo actúa a nivel local. En este caso, los eventos se asociarían por separado al .tab1 y al .tab2, por lo que obtendríamos una granularidad que live no permite.

stopPropagate() no funciona con live

Si el artículo citado anteriormente aconsejamos utilizar stopPropagate para evitar el mal uso del return false, con el método live no funciona: ya que live asocia el elemento al nivel más alto de la página, el stopPropagate no hace absolutamente nada (no quedan elementos por encima a los que propagarse).

Por el contrario, llamado al stopPropagate en un evento manejado con delegate, al permanecer encerrado dentro de su selector, funciona como se espera y evita que el evento se propage a los padres.

live() es más lento

Esto no debería sorprender: al tener que remontar constantemente la cadena de nodos hasta llegar al document, el uso de live es significativamente más lento que el de delegate. En aplicaciones con muchos niveles de profundidad, esta penalización puede llegar a ser importante.

live() no permite encadenar acciones

Aunque no solemos aprovechar al máximo (sobre todo por desconocimiento) la capacidad de jQuery para encadenar acciones, ésta permite reducir significativamente los códigos a la vez que mejora el rendimiento general. Por ejemplo, el siguiente código:

$('.myElement').click( function(){
  $this = $(this);
  $this.width('300');
  $this.height('300');
  $this.addClass('selected');
})

es similar a

$('.myElement').click( function(){
  $(this)
    .width('300')
    .height('300')
    .addClass('selected');
})

Encadenar acciones, además de resultar más limpio, permite aprovechar mejor la memoria del sistema: para cada acción, no tenemos que volver a seleccionar los elementos. Por otro lado, aunque tengamos la buena costumbre de cachearlo, con el encadenamiento encontramos que no es realmente necesario ni realizar esta asignación ( $this = $(this); ).

Pero con live no podemos confiar en esta funcionalidad: ya que el elemento se asocia al document, el contexto cambia y la cadena puede no funcionar como se espera.

Conclusiones

Tras revisar todos los puntos anteriores, podemos concluir que las mejores prácticas recomiendan no utilizar live en ningún caso. Tras la introducción de live, rápidamente se comprobó que era necesario una mejora sustancial en la API para la delegación de eventos. Ésta fue la razón que motivó la creación de delegate. Actualmente, jQuery continúa soportando el método live únicamente por motivos de retro compatibilidad con las versiones anteriores. La recomendación oficial al respecto es utilizar siempre delegate para la asocación y delegación de eventos.

Más:

{29} Comentarios.

  1. Berny Cantos

    En parte, $.live también permite seleccionar el ‘parent’, el límite hasta dónde el evento burbujeará, usando la sintaxis $(selector, element).live(…)
    Para mí lo que más me confunde de $.live es lo anti-intuitivo de su sintaxis.
    No conocía la existencia de $.delegate, así que le daré una ojeada y probablemente migre gran parte de mi código y de mis prácticas habituales.

    ¡Gracias!

  2. janogarcia

    Otro artículo más para guardar como referencia… Mil gracias!

    Todo lo que estás publicando sobre jQuery (http://www.etnassoft.com/category/jquery/) va en camino de convertirse en otra referencia de buenas practicas y patrones/antipatrones, un poco en la línea de lo que has plateando con http://www.javascriptboilerplate.com/

    Un saludo!

  3. guzman

    yo habitualmente no aplico los eventos hasta que todos los objetos del doom estan cargados.

    var aplicar_eventos =function(){ 
     $('.myListElement').bind( 'click', function(){
        alert(' Click! ');
      });
    }
    $(document).ready( function(){
      aplicar_eventos();
    } );
    

    y si es para uno que se añade en tiempo de ejecucion, los eventos forman parte del objeto que se crea, asi que hay que aplicarlos igual que las otras propiedades del objeto html

      $('.addNewListElement').click( function(){
        $('').appendTo('#myList');
    	aplicar_eventos();
      });
    

    delegate es util para ahorrarse el buscar los elementos por el doom, aunque para eso tambien se puede ahorrar parte del costo especificando mas la ruta

    var aplicar_eventos =function(){ 
     $('#myList .myListElement').bind( 'click', function(){
        alert(' Click! ');
      });
    }
    

    tambien tengo que decir que lo hago a «ojo», realmente nunca me he puesto a hacer pruebas de rendimiento 8)

    • Carlos Benítez

      Hola;
      desaconsejo completamente tú método porque estás realizando referencias circulares y reasignando a cada iteración nuevos ‘listeners’.

      Cada vez que invocas el aplicar_eventos(), recorres todo el DOM buscando los elementos coincidentes y creando con ello un nuevo ‘bind’. Al final, a cada iteración, los elementos más antiguos recibirán un nuevo trigger y el código se ejecutará n veces.

      Quiero decir que si añades un elemento en tiempo de ejecución y llamas a aplicar_eventos(), al pulsar sobre un elemento antiguo, recibirás 2 alerts: el primero y el nuevo.

      En todo caso, deberías hacer algo así:

      $('.myListElement').unbind('click').bind( 'click', function(){
        alert(' Click! ');
      });
      

      que eliminaría primero la referencia anterior para no crear una segunda. De todos modos, es un mal método con un rendimiento muy pobre.

      Saludos!

  4. Diego

    Te agradezco mucho el post. Tenía como tarea pendiente investigar un poco el tema del .live(). Lo utilizo bastante en una aplicación en la que estoy trabajando ahora y realmente, creo que ralentiza bastante. Mañana lo primero que haré será una refactorización de los .live() por .deletegate().

    Lo dicho, muchas gracias.

    • Carlos Benítez

      Sería interesante que luego comentaras la mejora en los tiempos para comprobar así qué grado de mejora obtienes 😉

      Un saludo!

  5. @jonasanx

    Yo igual que la persona que comento mas arriba, solo conocía de la existencia de delegate, siempre había utilizado $.fn.live, aunque nunca me ha dado problemas.

    Esto se sale de contexto, pero, ¿que herramientas se utilizan para medir el rendimiento de ciertos códigos de JavaScript?.

  6. Alberto Vilches

    Muy interesante, no conocía delegate. Con live hemos tenido problemas cuando refrescamos por ajax nuevas secciones de html con javascript con controles que tienen eventos. En este caso, lo que hacemos para que funcione es un die(‘click’) y un live(‘click’) y por ahora nos funciona, pero a veces no. Probaré el delegate.

  7. cocososo

    Uso jquery.taconite a tope y el live me va de perlas pero a veces tengo problemas con la propagacion de los eventos y al hacer click sobre un elemento de una capa se ejecuta el evento click de un elemento que esta en la capa inferior. Tengo que hacer pruebas con delegate.

    Aprovecho para darte las gracias por este y otros articulos que van de perlas para viejos como yo que estan aprendiendo javascript y jquery.

  8. Javi A

    «Aquí tenemos un ejemplo muy común. Pensemos en un widget compuesto por una capa con pestañas. Queremos que, cuando se pulse en una de esas pestañas, ocurra una acción. Como live asocia el evento al document, si incluimos dos de estos widgets en la misma página, cada vez que pinchemos en una pestaña el manejador de ambas será invocado. »

    No se si esto quiere decir que ese manejador se invoca dos veces o que ese manejador tiene que ser común para todas las pestañas. El primer caso no es cierto, por cada evento se invoca una sóla vez, independientemente del número de elementos del dom que tengan enlazado ese ‘live’… Si habláis del segundo caso, nada que decir, de acuerdo 🙂

    • Carlos Benítez

      Quiere decir que, independientemente de en qué tabla se haga click, el evento se disparará para todas las pestañas que cumplan con la condición del selector; esto es, todas aquellas capas o tablas que tienen, por ejemplo, la misma clase definida en el live().

      En este ejemplo de las pestañas, si al pulsar sobre una se despliega su contenido, con el ejemplo anterior, si tenemos dos tablas con pestañas en pantalla, al pulsar sobre «tabla 1, pestaña 1», se abrirían «tabla 1, pestaña 1» y «tabla 2, pestaña 1».

      Saludos!

  9. Antonio Stoner Zomb

    Hola, y gracias por tan buen artículo, me ha gustado bastante y me ha aclarado algo de lo no era conciente, 🙂

  10. Eduardo

    Hola Carlos. Me podrias explicar para que se usa el «.» en $(‘.addNewListElement’) o $(‘.myListElement’)?

    Un saludo

    • Carlos Benítez

      En jQuery, el ‘.’ indica que el selector que va a continuación se corresponde con una clase HTML. Por el contrario, el símbolo ‘#’ identificaría a un ID.

      Así tenemos que la siguiente capa,

      <div class="myClass" id="myID"></div>
      

      se puede seleccionar con jQuery usando:

      $('.myClass')
      

      o bien,

      $('#myID')
      

      Un saludo!

  11. Mario Gonzales

    Hola, estoy usando live para reordenar registros en medio de una paginacion que tambien usa ajax


    $(‘#product-table a.upregister’).live(‘click’, function() {
    var id = $(this).attr(‘alt’);
    var categoria_id = $(this).attr(‘title’);
    var parent = $(this).parent().parent();
    parent.prev().append().insertAfter(parent).show(‘slow’);
    /* Aqui si uso $.ajax de Jquery el elemento que ascendio se regresa ¿ sabes por que? yo lo he resuelto aplicando en esta linea javascript puro para usar ajax parar actualizar mi registro */
    return false;
    });

    Debo decir aqui que tambien use solo .click y tampoco funcionaba.

    Gracias de antemano doctor.

    • Carlos Benítez

      Puedes poner el ejemplo completo para ver cómo efectúas la llamada AJAX?
      En principio, aunque refactorizable, tú código es correcto.

      Un consejo; cuando se precisa remontar una cadena hacia atrás, en lugar de encadenar padres (.parent().parent().parent()…) es preferible usar el método parents() pasándole como argumento el selector del elemento que se desea alcanzar. Además de más ‘legible’, el rendimiento es también mucho más alto.

      Saludos!

  12. leifsk8er

    Hola Carlos, buenisimo el articulo. Te sigo desde hace poquito pero me encanta el blog y su tematica.

    Llevo medio año desarrollando algo parecido a un escritorio online, con sus widgets y demas, con lo que este articulo me viene como anillo al dedo. De hecho, seguramente debido a cosas mal programadas, ya que mi programacion no es orientada tampoco a objetos y puede que esten pasando memory leaks muy graves, estoy teniendo problemas de rendimiento con ordenadores de gama media/baja.

    Como uso mucho el live pienso que puede que este sea uno de los problemas que está relentizando la aplicacion, y haciendo que vaya «pesada».

    Te pego un enlace de la programacion JS de uno de los widgets. Se trata de un reproductor de musica.
    A ver si pudieras detectar, si tienes unos minutos claro, aparte del uso del delegate, que otras cosas estoy haciendo mal. Me serviria de muchisimo ya que todo el resto de lo que estoy haciendo basicamente en esencia de programacion es lo mismo.

    http://pastebin.com/XRLNr6ag

    Y aqui una imagen para que veas para que es el codigo
    http://oi55.tinypic.com/21lr5zs.jpg

    Un saludo!

    • Carlos Benítez

      Tiene muy buena pinta tu proyecto!
      A simple vista, el código no parece malo; está estructurado y utilizas los conceptos básicos de caché, etc… Sin embargo, es un código muy extenso como para analizarlo a simple vista y detectar errores de rendimiento, sobre todo cuando forma parte de un todo y, por tanto, está sujeto al comportamiento de más partes del sistema.

      Únicamente, con lo que tengo delante, el principal cuello de botella que te vas a encontrar en equipos de gama media, es precisamente el uso de jQuey junto con tantos el módulos del UI como he visto: slider, resizable, …
      Estas bibliotecas cargan mucho el DOM con capas y capas personalizadas que precisan para manejar las originales y crear efectos; además, hay que tener en cuenta que, si jQuery añade unos 180 métodos por cada elemento seleccionado, cuando sumamos la capa de UI, podemos irnos hasta los casi 300.

      Que quiere decir esto? pues que cada vez que realices una selección (cachees o no): $(‘mi_capa’), estás reservando la memoria del sistema equivalente al almacenamiento de esos 300 métodos. El recolector de basura de Javascript funciona medianamente bien, sobre todo desde IE7 para ir liberando memoria que se prevee no usar más. Sin embargo, si los elementos que estamos manejando son persistentes como parece ser tu caso (un dashboard), puede llegar un momento en que el sistema se vuelva inestable; especialmente en un Firefox con Firebug instalado.

      La recomendación en este caso es planificar: cuánto más tiene que crecer la aplicación? Cuántas funcionalidades faltan por implementar aún? Si son muchas, obviamente la arquitectura jQuery + UI puede resultar inviable y quizá sería interesante plantearse una arquitectura diferente. Por ejemplo, reescribiendo tu propio FW que solo utilice aquellos métodos imprescindibles. Otra opción sería sustituir jQuery por alternativas más livianas como ZeptoJS.

      De todos modos, sin una visión de la aplicación en su conjunto, es difícil ofrecer alternativas o trazar un plan de optimización.

      Solo tienes que tener en mente que una aplicación JS no puede crecer indefinidamente sin dicho plan: cuando se inicia, usar una biblioteca de terceros es fácil porque apenas hay carga. Pero si estamos creando todo un escritorio tipo W7, no podemos depender de estas herramientas dada la enorme cantidad de recursos que requieren para si cuando las estructuras se vuelven complejas y pesadas.

      Suerte con el proyecto!

  13. Omar

    Debería haber una actualización en esta entrada o en el blog en donde se explique que desde jQuery 1.7 la función .delegate ha sido sustituida por la función .on.

  14. Eliiq

    Hola muy buen post. con respecto a los comentarios sobre usar un metodo «aplicar_eventos», creo que es muy pesado estar reasignando los eventos a todos los elementos cada vez que se crea uno nuevo.

    lo que yo siempre hago es definir mi callback. y luego asignarlo al elemento que lo requiera.

    function callback(e){
        console.log(e);
        console.log(jQuery(this));
    }
    
    jQuery('.myListElement').click(callback);
    

    Y al agregar nuevos elementos:

    jQuery('.addNewListElement').click( function(){
        var $new=jQuery('').addClass('foo').click(callback);
           jQuery('body').append($new);
      });
    

    Saludos

  15. Pardi

    Buenas, según he entendido en tu post, necesitas un elemento que ya existiera en la carga de la página para efectuar sobre él el delegate, en el caso del ejemplo, $('#myList')
    ¿Qué ocurre si no tengo ningún elemento presente en la carga y todos se generan dinámicamente? Esto me pasa porque cargo una página completa con un formulario en un modalBox y luego quiero desde la página «padre» capturar el click sobre un botón de dicho formulario…

    Mi código de pruebas es este:

    //Para cargar el fancybox con el formulario
    jQuery('#new_category').live('click', function(e){
    
      jQuery.fancybox({
        titleShow     : false,
        href: jQuery(this).attr('href'),
        padding	: 20,
        type: 'iframe',
        width: "75%",
        height: "95%",
        live : true,
        centerOnScroll: true,
        onComplete : function(){
            alert('cargado'); //esto SI funciona
        }
    
      });
        return false;
    });
    
    //Para controlar el click sobre el botón de envío del formulario
    //el id utilizado es correcto, he probado con delegate y on con la misma suerte
    jQuery('#new_category_button').live('click', function(){ 
      alert('foka'); //NO funciona
      jQuery('#new_category').ajaxSubmit({
        dataType: 'json',
        success: function(responseText, statusText, xhr){
            alert('yeah');
            return false;
        },
        error: function(responseText, statusText, xhr){
            alert('problems');
            return false;
        }
      });
      return false;
    }); 
    

    Muchas gracias por echarle un ojo a esto, y siento si la explicación no es correcta. Un saludo!

  16. Pardi

    Soy el tarugo de arriba, ¿qué etiquetas usais para meter código? Saludos!

  17. Tomás

    revisa lo de encadenar acciones, lo he probado y no me funciona. He probado a quitar el «;» al final de las líneas intermedias y sí me funciona.
    Ej:

    span.removeClass(«editando»)
    .addClass(«edit»);//encadenando

    Gracias !

    Pd: ¿podeis añadir el enlace de la entrada de la función «.on» en esta entrada? pues parece el método definitivo más apropiado.

    • Carlos Benítez

      Tienes razón; se me escaparon en su día los punto y coma.
      Gracias!

  18. Luis

    Excelente alternativa la del evento Bind para darle vida a los elementos creados en tiempo real, ya que con IE7 no anda el evento live, en las nuevas versiones del jquery ya se maneja el evento «on» en ves de live que igual anda de pelos en los nuevos navegadores, en mi caso como es una app específicamente para IE7 el bind me vino bien, gracias por los aportes 😉

  19. antoine

    Muchisimas gracias por tan excelente artículo, perfectamente explicado y detallado con ejemplos.
    Se me presentó este problema con unos elementos que se insertan dinámicamente al utilizar la paginación, y no pensaba que fuese a encontrar la solución tan fácilmente.
    Gracias a tu artículo entendí el fondo del problema y la mejor solución.
    Añadiré tu página a mis favoritos.
    Saludos.

  20. Alexis Tamariz Hernandez

    Carlos, te lo agradezco en verdad.
    La explicación que has hecho en este post me ha servido de gran manera.
    Estuve sufriendo toda esta semana intentando hallar la solución a este problema que parece simple, aunque es para llorar.

    Gran aporte!! D;
    Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *