Rediseñando el método each() en jQuery

30 Mar 2011

Introducción

Una de las funcionalidades más interesantes de jQuery es la posibilidad de recorrer todos los elementos de un set (colección de elementos) mediante el método interno each. Gracias a esta función, podemos realizar cualquier acción sobre cada uno de los elementos que un selector haya recopilado.

Su sintaxis es la siguiente:

jQuery.each( collection, callback(indexInArray, valueOfElement) )

Donde:

collection es el objeto o array que queremos recorrer.
callback es la función que queremos aplicar sobre cada uno de los elementos del objeto.
indexInArray corresponde, dentro del callback, al índice del elemento sobre el que iteramos en un momento dado.
valueOfElement es el valor del elemento.

Uso básico

Como es posible añadir este método a una cadena de acciones, su uso más frecuente es el siguiente:

$('a').each( function(){
  // Do something...
});

Con el ejemplo anterior, estaríamos seleccionando todas las etiquetas a (anchors) dentro de nuestro documento para pasar a recorrerlas una a una.

Por lo general, el fin de este método es aplicar una acción sobre cada uno de los elementos seleccionados, por lo que necesitamos de algún modo referenciarlos. Lo más común es recurrir a this y cachearlo:

$('a').each( function(){
  var $this = $(this);
  $this.css( 'text-decoration' , 'underline' );
});

NOTA: Precachear los objetos jQuery en lugar de referenciarlos directamente es una buena práctica ya que permite guardar la referencia para usarla más adelante evitando volver a localizar el elemento en el DOM. Para más información, puede consultarse “Cacheando objetos en jQuery“.

El problema

El gran problema que presenta la estructura anterior es que puede resultar muy pesada recorriendo grandes conjuntos de datos. Esto es porque, en cada iteración, tiene que volver a localizarse el elemento en el DOM y seleccionarse, provocando así la correspondiente penalización en rendimiento.

Una forma de mejorar notablemente la velocidad de each, es crear un placeholder o marcador de contexto que delimite la ubicación del elemento sobre el que estamos iterando. Sería algo equivalente a cuando en un selector normal, indicamos el contexto en el que queremos buscar:

$('a', '#myLayer'); // Select all <a> elements within #myLayer context

Este método resultaría mucho más rápido en aquellos casos en los que necesitemos actuar directamente sobre $this aunque más lento cuando no. Por ello, lo más interesante es disponer de ambas soluciones y utilizar cada una según lo necesitemos.

La solución idónea es crear un sencillo plugin para nuestro nuevo each.

Método each optimizado

El siguiente ejemplo se lo debemos a Ben Alman (@cowboy) basándose en un trabajo previo de James Padolsey:

(function($) {
  var $this = $([1]);
 
  $.fn.each2 = function( fn ) {
    var i = -1;
 
    while (
      ( $this.context = $this[0] = this[++i] )
      && fn.call( $this[0], i, $this ) !== false
    ) {}
    return this;
  };
})(jQuery);

Eso es todo. Su uso es muy sencillo:

$('a').each2(function( i, $this ){
  $this.css('text-decoration', 'underline');
});

this ahora está contenido en la variable $this la cual hemos predefinido de forma interna en nuestro plugin después de asignarle un contexto. No hemos necesitado re seleccionarla y el incremento en rendimiento es más que notable cuando trabajamos con conjuntos grandes.

Para ver la tabla comparativa entre este método y el original, podemos visitar la página de pruebas montada en JsPerf.

Conclusión

Los tests son concluyentes: con el nuevo método, ganamos del orden de un 500% de rendimiento, algo que no puede ser pasado por alto cuando se trata de aplicaciones de gran arquitectura o muy exigentes en cuanto a cálculo.

Más:

Solo un marciano comentando!

  1. Zzarcon

    No acabo de comprender el funcionamiento del each2, no se supone que internamente tambien cachea la variable this? y si no lo hace no entiendo como funciona :(, aunque bueno si dices que es 500% mas rápido abra que empezar a usarlo 🙂 un saludo y enorabuena por tus articulos!!

Deja un comentario

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