Comunidad de diseño web y desarrollo en internet

Crear múltiples temporizadores con jQuery

Estimados amigos, voy a intentar explicar cómo crear múltiples temporizadores con jQuery en una página de manera eficiente y sin que resulten una carga para la memoria, por supuesto, siempre dentro de ciertos límites (No prometo que 2000 temporizadores funcionen bien, no he probado).

Para el caso vamos a utilizar un plugin que he creado para una web que he hecho en Wordpress, os voy a hacer entrar en contexto para entender mejor este caso en concreto:

Esta web vende productos por ebay en subastas (además de otras plataformas) y el plug-ing se encarga de que en el front office del wordpress aparezcan las subastas que haya activas en el momento de la visita, que se muestre un temporizador con el tiempo que le queda a cada subasta haciendo su correspondiente cuenta atrás, que se muestre su precio actual y que se rellene un tooltip que he creado, indicando el tiempo en el que se actualizaron los precios.



Podéis revisarlo en vivo aquí: Ruralyt

  • No vamos a ver nada de wordpress, sólo javascript.[/i]
  • Actualmente sigo haciendo cambios en la web, es posible que el contenido de las imágenes difiera de lo encontrado en la web.
  • El código está escrito en modo no-coflict, quien quiera puede sentirse libre de reemplazar los jQuery por $


Para empezar, vamos a revisar el HTML base de los objetos que vemos en las imágenes. Esta será nuestra lista y tendrá definido el tiempo de última actualización de los datos.

Código :

<!--
    El atributo updated nos valdrá
    para saber cuándo se actualizaron los datos.
-->
<ul class="ebAuctions" updated="1361888249">
    <li timestamp=....></li>
</ul>

Cada temporizador será un item de nuestra lista

Código :

<li timestamp="1361905967" tooltip="someText">
    <img src="productImage" alt="imageAlt">
    <p>TITULO</p>
    <p>PRECIO ACTUAL</p>
    <div>
        <p>
            Tiempo restante:
            <span class="timer_days">TIMER</span>D
            <span class="timer_hours">TIMER</span>H
            <span class="timer_minutes">TIMER</span>M
            <span class="timer_seconds">TIMER</span>S
        </p>
    </div>
</li>

Bien, pues manos a la obra. Empezaremos por definir un array donde almacenaremos todos nuestros temporizadores.

Código :

/**
    Este será el array que revisaremos
    cuando actualicemos los temporizadores
**/
var ebayTimers = Array();

Cuando el DOM este completamente listo, inicializaremos nuestros contadores mediante la función caEbInitCounters

Código :

jQuery(document).ready(caEbInitCounters);

Ahora crearemos la función caEbInitCounters que definirá los valores de tiempo referentes al momento de "ahora", y mediante un selector y un bucle each, formará los contadores. Posteriormente los iniciará.

Código :

/*No hacen falta parámetros ni la función devuelve nada*/
function caEbInitCounters () {
    /**    
        Obtenemos el valor en segundos
        de la última vez que se actualizaron los datos,
        lo sacaremos de nuestro ul principal
        y lo guardaremos en la variable "catched"
    **/
    var catched = parseInt(jQuery('.ebAuctions').attr('updated')),
        /**
            Generamos la fecha de "ahora"
            y la almacenamos en la variable "date"
        **/
        date = new Date();
    
    /**
        Creamos una variable sin declarar,
        para que tenga ámbito global
        esta almacenará la fecha de hoy en segundos
        la llamermos "miliseconds" (aunque serán segundos)

        Dado que javascript devuelve milisegundos
        dividiremos por 1000 para trabajar en segundos
    **/
    miliseconds = (parseInt(date.getTime())/1000);
    
    /**
        Ahora a la fecha de "ahora",
        le restaremos la de última actualización
        para obtener en segundos el tiempo pasado
        desde que se actualizaron los datos
    **/
    var timePassed = miliseconds-catched;
}

Calculemos los tiempos, primero lo vamos a hacer de manera normal y luego vamos a crear una función que nos ayudará en todo el script

Código :

/**
    calculamos el tiempo pasado en minutos
    y lo pasamos por Math.floor para que quite los decimales

    OJO: no usamos parseInt, ni ceil,
    pues lo que queremos es el resultado
    sin decimales ni redondeado

    Para calcular los minutos totales
    dividimos los segundos entre 60
**/
minutes = Math.floor(timePassed/60),

/**
    Para calcular los segundos,
    podríamos calcular el tiempo pasado
    menos los minutos*60
**/
seconds = Math.floor(timePassed-(minutes*60));

/*Así será más eficiente*/
seconds = Math.floor(timePassed%60);

Para hacer esto último vamos a crear una función que nos devuelva ambos valores como un array, esta función nos ayudará más adelante y la llamaremos division.

Código :

/**
    Le pasaremos 2 números
    y nos dará el resultado sin decimales de su divión
    y el resto de la misma, ambos valores en un array

    @a -> INT - Dividendo
    @b -> INT - Divisor

    :Array() -> [Floor(resultado), Floor(resto)]
**/
function division (a, b) {
    return Array(Math.floor(a/b), Math.floor(a%b))
}

Y ahora los últimos datos que hemos obtenido los sacaríamos así:

Código :

var minsSecs = division(timePassed, 60);

//Y recuperamos los minutos y los segundos del array
minutes = minsSecs[0],
seconds = minsSecs[1];

Y para terminar la función crearemos el bucle que leerá nuestros elementos y los convertirá en temporizadores extendiendo sus atributos en el DOM. También crearemos la llamada a la función que lo iniciará todo (está función la crearemos más alante, la llamaremos inter).

Código :

/**
    Ahora mediante un selector jQuery,
    y un bucle each mandamos cada resultado
    del selector a la función "fill_updated"
    para convertirlos en contadores
**/
jQuery('[timestamp]').each(fill_updated);

//Iniciamos la cuenta atrás
inter();

Bien, veamos la función acabada

Código :

function caEbInitCounters () {
    var catched = parseInt(jQuery('.ebAuctions').attr('updated')),
        date = new Date();

    miliseconds = (parseInt(date.getTime())/1000);

    var timePassed = miliseconds-catched,
        minsSecs = division(timePassed, 60);

    minutes = minsSecs[0];
    seconds = minsSecs[1];

    jQuery('[timestamp]').each(fill_updated);

    inter();
}

Comencemos con la creación de la función fill_updated, esta función se encargará de extender las propiedades de nuestros objetos (los que tengan el atributo html [timestamp]) en el DOM, para convertirlos en temporizadores.

Código :

/**
    FUNCTION fill_updated, dado que esta función la ejecutamos
    desde un selector con each, podemos declarar la variable
    con el objeto que necesitamos como segundo parámetro.
    
    También podríamos seleccionar este objeto con $(this),
    si no cogiésemos los parámetros
    
    @i -> valor del bucle, como la función se ejecutará
        tantas veces como resultados de el selector,
        la i valdrá 0 en el primer elemento 1 en el segundo...
        igual que un bucle (no la usaremos).

    @object -> el objeto resultado del selector
        en cada paso de ejecución de la función
**/
function fill_updated (i, object) {
    /**
        Primero crearemos las variables de tiempo
        que necesitamos para crear el contador
        
        TimeStamp: La fecha de finalización de ese objeto
        total: el tiempo en segundos que quedan
            (segundos de finalización - segundos de "ahora")
        Mdays: La cantidad de días que quedan
        Mhours: La cantidad de horas que quedan
        Mminutes: La cantidad de minutos que quedan
    **/
    
    //Extraemos el timestamp de nuestro objeto
    var timestamp = parseInt(jQuery(object).attr('timestamp')),
        total = parseInt(timestamp-miliseconds),
        
        /**
            Usamos nuestra función división
            para obtener resultado y resto
            Para obtener los días dividimos
            nuestros segundos totales por (60secs*60mins*24hor)
        **/
        Mdays = division(total, (60*60*24)),
        
        /**
            Para las horas, el resto de la división anterior
            dividido por (60secs*60mins)
        **/
        Mhours = division(Mdays[1], (60*60)),
        
        /**
            Y para los minutos y segundos,
            volvemos a usar el resto de la anterior
            y dividimos por (60secs)
        **/
        Mminutes = division(Mhours[1], 60);
    
    /**
        Ahora vamos a extender nuestro objeto del selector
        con una nueva propiedad 'timers'
        que a su vez será un objeto con más propiedades
        
        timers->seconds,
        timers->minutes,
        timers->hours,
        timers->days
        Cogeremos como valores los que acabamos de calcular
        estas son propiedades del tiempo restante de cada objeto
        para legar a 0:00:00:00
        
        Object es el objeto que recibimos del each,
        También podría ser "this" (no "$(this)"),
        en caso de que no recibiéramos los parámetros
        con la función, como se ha comentado antes
    **/
    object.timers = {
        seconds: Mminutes[1],
        minutes: Mminutes[0],
        hours: Mhours[0],
        days: Mdays[0],
        
        /**
            ->Umin,
            ->Usec
            estas propiedades definirán el tiempo que hace
            que se actualizaron los datos
            
            Dado que en este caso concreto
            el máximo de este tiempo serán 10 minutos,
            no nos hacen falta ni horas ni días
            
            Estos 2 valores se reciben de la primera función
        **/
        Umin: minutes,
        Usec: seconds
    };
    
    /**
        ->itself
        Será el propio objeto seleccionado por jQuery
    **/
    object.itself = jQuery(object);
    
    /**
        ->seconds,
        ->minutes,
        ->hours,
        ->days
        el <span> correspondiente seleccionado
    **/
    object.seconds = jQuery('.timer_seconds', object);
    object.minutes = jQuery('.timer_minutes', object);
    object.hours = jQuery('.timer_hours', object);
    object.days = jQuery('.timer_days', object);
}

Veamos cómo queda el objeto limpio, aún sin funciones. Como podemos observar, aquí hacemos uso pleno de nuestra función division.

Código :

function fill_updated (i, object) {
    var timestamp = parseInt(jQuery(object).attr('timestamp')),
        total = parseInt(timestamp-miliseconds),
        Mdays = division(total, (60*60*24)),
        Mhours = division(Mdays[1], (60*60)),
        Mminutes = division(Mhours[1], 60);
        
    object.timers = {
        seconds: Mminutes[1],
        minutes: Mminutes[0],
        hours: Mhours[0],
        days: Mdays[0],
        Umin: minutes,
        Usec: seconds
    };
    
    object.itself = jQuery(object);
    object.seconds = jQuery('.timer_seconds', object);
    object.minutes = jQuery('.timer_minutes', object);
    object.hours = jQuery('.timer_hours', object);
    object.days = jQuery('.timer_days', object);
}

Retomemos la función fill_updated, ahora vamos a extender los objetos con la función que actualizará sus temporizadores internamente.

Código :

function fill_updated (i, object) {
    [...]
    
    /**
        Ahora extenderemos el objeto con una función
        como ya hemos dicho, esta función actualizará
        los temporizadores de cada objeto internamente
    **/
    object.actualizarTimers = function () {
        /**
            Primero extraeremos los valores que necesitamos
            de nuestro objeto para nuestra comodidad
        **/
        var secondsC = object.timers.seconds,
            minutesC = object.timers.minutes,
            hoursC = object.timers.hours,
            daysC = object.timers.days;
            Usec = object.timers.Usec;
            Umin = object.timers.Umin;
        
        /**
            Empezaremos por la parte que actualizará
            el temporizador del tiempo que hace
            que se actualizaron los datos
            
            Si los segundos son iguales a 59
        **/
        if (Usec == 59) {
            //Incrementamos en 1 la propiedad timers.Umin
            object.timers.Umin++;
            
            //Ponemos a 0 la propiedad de los segundos timers.Usec
            object.timers.Usec = 0;
        }
        
        /**
            Si no, simplemete
            incrementaremos 1 la propiedad de los segundos
        **/
        else object.timers.Usec++;
        
        /**
            Ahora programaremos la parte referente a los contadores
            del tiempo restante de subasta
            
            Si los segundos son iguales a 0
        **/
        if (secondsC == 0) {
        
            //Si los minutos son iguales a 0
            if (minutesC == 0) {
            
                //Si las horas son iguales a 0
                if (hoursC == 0) {
                
                    //Si los días son iguales a 0
                    if(daysC == 0) {
                        /**
                            Salimos de la función sin hacer nada
                            pues la subasta ha acabado
                        **/
                        return;
                    }
                    
                    //Si lo anterior pero aún quedan días
                    else {
                        //Igualamos a 23 la propiedad de las horas
                        object.timers.hours = 23;
                        
                        //restamos 1 a la propiedad de los días
                        object.timer.days--;
                    }
                }
                
                //Si lo anterior pero aún quedan horas
                else {
                    //Igualamos a 59 la propiedad de los minutos
                    object.timer.minutes = 59;
                    
                    //restamos 1 a las horas
                    object.timer.hours--;
                }
            }
            
            //Si lo anterior pero aún quedan minutos
            else {
                //Igualamos a 59 la propiedad de los segundos
                object.timers.seconds = 59;
                
                //restamos 1 a la propiedad de los minutos
                object.timers.minutes--;
            }
        }
        
        /**
            Si lo anterior pero aún quedan segundos
            restamos 1 a la propiedad de los segudos
        **/
        else object.timers.seconds--;
        
        /**
            Y Ahora llamaremos a la función del objeto
            que crearemos a continuación y que se encargará
            de actualizar el HTML visible por el usuario
        **/
        object.actualizarHTML();
    };
}

Bueno pues ya sólo nos queda la parte que verá el usuario y el temporizador que lo moverá todo. Vamos con la última función que incorporará nuestro objeto y que se encargará de mostrar al usuario en todo momento las cuentas atrás y el tiempo de actualización.

Código :

function fill_updated (i, object) {
    [...]
     
    //Definimos la función actualizarHTML
    object.actualizarHTML = function () {
        
        /**
            De nuevo extraemos los valores
            de las propiedades del objeto
        **/
        var seco = object.timers.seconds,
            minu = object.timers.minutes,
            hour = object.timers.hours,
            days = object.timers.days,
            Usec = object.timers.Usec,
            Umin = object.timers.Umin;
        
        /**
            Lo primero que haremos será actulizar el tooltip
            con el tiempo de actualización
        **/    
        object.itself.attr(
            'tooltip',
            'Lo precios se actualizan cada 10 minutos aprox.'
            +'<hr>Actualizado hace: '
            +Umin+'m '
            
            //Si los segundos son inferiores a 10 le añadimos el 0
            +(Usec < 10 ? '0' : '')
            +Usec+'s'
        );

        /**
            Actualizamos el resto de los valores
            para la visibilidad del usuario
        **/
        object.seconds.html(seco > 9 ? seco : '0'+seco);
        object.minutes.html(minu > 9 ? minu : '0'+minu);
        object.hours.html(hour > 9 ? hour : '0'+hour);
        object.days.html(object.timers.days);
    };
    
    /**
        Por último, guardamos nuestro objeto completo,
        en el array "ebayTimers" que creamos al principio.
        De esta manera cuando tengamos que actualizar,
        no tendremos que recurrir a selectores,
        sino directamente a los objetos
        almacenados en nuestro array
    **/
    ebayTimers.push(object);
}

Veamos la última función entera.

Código :

function fill_updated (i, object) {
    var timestamp = parseInt(jQuery(object).attr('timestamp')),
        total = parseInt(timestamp-miliseconds),
        Mdays = division(total, (60*60*24)),
        Mhours = division(Mdays[1], (60*60)),
        Mminutes = division(Mhours[1], 60);
        
    object.timers = {
        seconds: Mminutes[1],
        minutes: Mminutes[0],
        hours: Mhours[0],
        days: Mdays[0],
        Umin: minutes,
        Usec: seconds
    };
    
    object.itself = jQuery(object);
    object.seconds = jQuery('.timer_seconds', object);
    object.minutes = jQuery('.timer_minutes', object);
    object.hours = jQuery('.timer_hours', object);
    object.days = jQuery('.timer_days', object);
    
    object.actualizarTimers = function () {
        var secondsC = object.timers.seconds,
            minutesC = object.timers.minutes,
            hoursC = object.timers.hours,
            daysC = object.timers.days;
            Usec = object.timers.Usec;
            Umin = object.timers.Umin;
            
        if (Usec == 59) {
            object.timers.Umin++;
            object.timers.Usec = 0;
        }
        
        else object.timers.Usec++;
        
        if (secondsC == 0) {
            if (minutesC == 0) {
                if (hoursC == 0) {
                    if(daysC == 0) {
                        return;
                    }
                    else {
                        object.timers.hours = 23;
                        object.timer.days--;
                    }
                }
                else {
                    object.timer.minutes = 59;
                    object.timer.hours--;
                }
            }
            else {
                object.timers.seconds = 59;
                object.timers.minutes--;
            }
        }
        else object.timers.seconds--;
        
        object.actualizarHTML();
    };
    
    object.actualizarHTML = function () {
        var seco = object.timers.seconds,
            minu = object.timers.minutes,
            hour = object.timers.hours,
            days = object.timers.days,
            Usec = object.timers.Usec,
            Umin = object.timers.Umin;
            
        object.itself.attr(
            'tooltip',
            'Lo precios se actualizan cada 10 minutos aprox.<hr>Actualizado hace: '
            +Umin+'m '
            +(Usec < 10 ? '0' : '')
            +Usec+'s'
        );    
            
        object.seconds.html(seco > 9 ? seco : '0'+seco);
        object.minutes.html(minu > 9 ? minu : '0'+minu);
        object.hours.html(hour > 9 ? hour : '0'+hour);
        object.days.html(object.timers.days);
    };
    
    ebayTimers.push(object);
}

Ahora crearemos la función que disparará la actualización de los temporizadores una vez por segundo.

Código :

function inter () {
    update_timers();
    setTimeout(inter, 1000);
}

Y por último la función que se encargará de actualizar todos los timers almacenados en el array.

Código :

function update_timers () {
    //Por cada objeto en nuestro array
    for (object in ebayTimers)
        //Ejecutamos su propiedad de actualizar
        ebayTimers[object].actualizarTimers();
}

Por último, veámoslo todo junto sin comentarios.

Código :

jQuery(document).ready(caEbInitCounters);

var ebayTimers = Array();

function caEbInitCounters () {
   var catched = parseInt(jQuery('.ebAuctions').attr('updated')),
      date = new Date();
      
   miliseconds = (parseInt(date.getTime())/1000);
   
   var timePassed = miliseconds-catched,   
      minsSecs = division(timePassed, 60);
        
   minutes = minsSecs[0];
   seconds = minsSecs[1];
   
   jQuery('[timestamp]').each(fill_updated);
   
   inter();
}

function fill_updated (i, object) {
   var timestamp = parseInt(jQuery(object).attr('timestamp')),
      total = parseInt(timestamp-miliseconds),
      Mdays = division(total, (60*60*24)),
      Mhours = division(Mdays[1], (60*60)),
      Mminutes = division(Mhours[1], 60);
      
   object.timers = {
      seconds: Mminutes[1],
      minutes: Mminutes[0],
      hours: Mhours[0],
      days: Mdays[0],
      Umin: minutes,
      Usec: seconds
   };
   
   object.itself = jQuery(object);
   object.seconds = jQuery('.timer_seconds', object);
   object.minutes = jQuery('.timer_minutes', object);
   object.hours = jQuery('.timer_hours', object);
   object.days = jQuery('.timer_days', object);
   
   object.actualizarTimers = function () {
      var secondsC = object.timers.seconds,
         minutesC = object.timers.minutes,
         hoursC = object.timers.hours,
         daysC = object.timers.days;
         Usec = object.timers.Usec;
         Umin = object.timers.Umin;
         
      if (Usec == 59) {
         object.timers.Umin++;
         object.timers.Usec = 0;
      }
      
      else object.timers.Usec++;
      
      if (secondsC == 0) {
         if (minutesC == 0) {
            if (hoursC == 0) {
               if(daysC == 0) {
                  return;
               }
               else {
                  object.timers.hours = 23;
                  object.timer.days--;
               }
            }
            else {
               object.timer.minutes = 59;
               object.timer.hours--;
            }
         }
         else {
            object.timers.seconds = 59;
            object.timers.minutes--;
         }
      }
      else object.timers.seconds--;
      
      object.actualizarHTML();
   };
   
   object.actualizarHTML = function () {
      var seco = object.timers.seconds,
         minu = object.timers.minutes,
         hour = object.timers.hours,
         days = object.timers.days,
         Usec = object.timers.Usec,
         Umin = object.timers.Umin;
         
      object.itself.attr('tooltip', 'Lo precios se actualizan cada 10 minutos aprox.<hr>Actualizado hace: '+Umin+'m '+(Usec < 10 ? '0' : '')+Usec+'s');   
         
      object.seconds.html(seco > 9 ? seco : '0'+seco);
      object.minutes.html(minu > 9 ? minu : '0'+minu);
      object.hours.html(hour > 9 ? hour : '0'+hour);
      object.days.html(object.timers.days);
   };
   
   ebayTimers.push(object);
}

function division (a, b) {return Array(Math.floor(a/b), Math.floor(a%b))}

function inter () {
   update_timers();
   setTimeout(inter, 1000);
}

function update_timers () {
   for (object in ebayTimers) {
      ebayTimers[object].actualizarTimers();
   }
}

Espero que a alguien le sea de utilidad.
un saludo a todos.

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Publica tu comentario

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate