21 de noviembre de 2022

Planificación: setTimeout y setInterval

Podemos decidir ejecutar una función no ahora, sino un determinado tiempo después. Eso se llama “planificar una llamada”.

Hay dos métodos para ello:

  • setTimeout nos permite ejecutar una función una vez, pasado un intervalo de tiempo dado.
  • setInterval nos permite ejecutar una función repetidamente, comenzando después del intervalo de tiempo, luego repitiéndose continuamente cada intervalo.

Estos métodos no son parte de la especificación de JavaScript. Pero la mayoría de los entornos tienen el planificador interno y proporcionan estos métodos. En particular, son soportados por todos los navegadores y por Node.js.

setTimeout

La sintaxis:

let timerId = setTimeout(func|código, [retraso], [arg1], [arg2], ...)

Parámetros:

func|código
Una función o un string con código para ejecutar. Lo normal es que sea una función. Por razones históricas es posible pasar una cadena de código, pero no es recomendable.
retraso
El retraso o delay antes de la ejecución, en milisegundos (1000 ms = 1 segundo), por defecto 0.
arg1, arg2
Argumentos para la función

Por ejemplo, este código llama a sayHi() después de un segundo:

function sayHi() {
  alert('Hola');
}

setTimeout(sayHi, 1000);

Con argumentos:

function sayHi(phrase, who) {
  alert( phrase + ', ' + who );
}

setTimeout(sayHi, 1000, "Hola", "John"); // Hola, John

Si el primer argumento es un string, JavaScript crea una función a partir de él.

Entonces, esto también funcionará:

setTimeout("alert('Hola')", 1000);

Pero no se recomienda usar strings, use funciones de flecha en lugar de ello:

setTimeout(() => alert('Hola'), 1000);
Pasa una función, pero no la ejecuta

Los principiantes a veces cometen un error al agregar paréntesis () después de la función:

// ¡mal!
setTimeout(sayHi(), 1000);

Eso no funciona, porque setTimeout espera una referencia a una función. Y aquí sayHi() ejecuta la función, y el resultado de su ejecución se pasa a setTimeout. En nuestro caso, el resultado de sayHi() es undefined (la función no devuelve nada), por lo que no habrá nada planificado.

Cancelando con clearTimeout

Una llamada a setTimeout devuelve un “identificador de temporizador” timerId que podemos usar para cancelar la ejecución.

La sintaxis para cancelar:

let timerId = setTimeout(...);
clearTimeout(timerId);

En el siguiente código, planificamos la función y luego la cancelamos (cambiamos de opinión). Como resultado, no pasa nada:

let timerId = setTimeout(() => alert("no pasa nada"), 1000);
alert(timerId); // identificador del temporizador

clearTimeout(timerId);
alert(timerId); // mismo identificador (No se vuelve nulo después de cancelar)

Como podemos ver en la salida alert, en un navegador el identificador del temporizador es un número. En otros entornos, esto puede ser otra cosa. Por ejemplo, Node.js devuelve un objeto de temporizador con métodos adicionales.

De nuevo: no hay una especificación universal para estos métodos.

Para los navegadores, los temporizadores se describen en la sección timers del estándar HTML.

setInterval

El método setInterval tiene la misma sintaxis que setTimeout:

let timerId = setInterval(func|código, [retraso], [arg1], [arg2], ...)

Todos los argumentos tienen el mismo significado. Pero a diferencia de setTimeout, ejecuta la función no solo una vez, sino regularmente después del intervalo de tiempo dado.

Para detener las llamadas, debemos llamar a ‘clearInterval (timerId)’.

El siguiente ejemplo mostrará el mensaje cada 2 segundos. Después de 5 segundos, la salida se detiene:

// repetir con el intervalo de 2 segundos
let timerId = setInterval(() => alert('tick'), 2000);

// después de 5 segundos parar
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
El tiempo pasa mientras se muestra ‘alerta’

En la mayoría de los navegadores, incluidos Chrome y Firefox, el temporizador interno continúa “marcando” mientras muestra “alert/confirm/prompt”.

Entonces, si ejecuta el código anterior y no descarta la ventana de ‘alerta’ por un tiempo, la próxima ‘alerta’ se mostrará de inmediato. El intervalo real entre alertas será más corto que 2 segundos.

setTimeout anidado

Hay dos formas de ejecutar algo regularmente.

Uno es setInterval. El otro es un setTimeout anidado, como este:

/** en vez de:
let timerId = setInterval(() => alert('tick'), 2000);
*/

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

El setTimeout anterior planifica la siguiente llamada justo al final de la actual (*).

El setTimeout anidado es un método más flexible que setInterval. De esta manera, la próxima llamada se puede planificar de manera diferente, dependiendo de los resultados de la actual.

Ejemplo: necesitamos escribir un servicio que envíe una solicitud al servidor cada 5 segundos solicitando datos, pero en caso de que el servidor esté sobrecargado, deber aumentar el intervalo a 10, 20, 40 segundos…

Aquí está el pseudocódigo:

let delay = 5000;

let timerId = setTimeout(function request() {
  ...enviar solicitud...

  if (solicitud fallida debido a sobrecarga del servidor) {
    //aumentar el intervalo en la próxima ejecución
    delay *= 2;
  }

  timerId = setTimeout(request, delay);

}, delay);

Y si las funciones que estamos planificando requieren mucha CPU, entonces podemos medir el tiempo que tarda la ejecución y planificar la próxima llamada más tarde o más temprano.

setTimeout anidado permite establecer el retraso entre las ejecuciones con mayor precisión que setInterval.

Comparemos dos fragmentos de código. El primero usa setInterval:

let i = 1;
setInterval(function() {
  func(i++);
}, 100);

El segundo usa setTimeout anidado:

let i = 1;
setTimeout(function run() {
  func(i++);
  setTimeout(run, 100);
}, 100);

Para setInterval el planificador interno se ejecutará func(i++) cada 100ms:

¿Te diste cuenta?

¡El retraso real entre las llamadas de func para setInterval es menor que en el código!

Eso es normal, porque el tiempo que tarda la ejecución de func “consume” una parte del intervalo.

Es posible que la ejecución de func sea más larga de lo esperado y demore más de 100 ms.

En este caso, el motor espera a que se complete func, luego verifica el planificador y, si se acabó el tiempo, lo ejecuta de nuevo inmediatamente.

En caso límite, si la ejecución de la función siempre demora más que los ms de retraso, entonces las llamadas se realizarán sin pausa alguna.

Y aquí está la imagen para el setTimeout anidado:

El setTimeout anidado garantiza el retraso fijo (aquí 100ms).

Esto se debe a que se planea una nueva llamada al final de la anterior.

Recolección de basura y setInterval/setTimeout callback

Cuando se pasa una función en setInterval / setTimeout, se crea una referencia interna y se guarda en el planificador. Esto evita que la función se recolecte, incluso si no hay otras referencias a ella…

// la función permanece en la memoria hasta que el planificador la llame
setTimeout(function() {...}, 100);

Para setInterval, la función permanece en la memoria hasta que se invoca clearInterval.

Hay un efecto secundario. Una función hace referencia al entorno léxico externo, por lo tanto, mientras vive, las variables externas también viven. Pueden tomar mucha más memoria que la función misma. Entonces, cuando ya no necesitamos la función planificada es mejor cancelarla, incluso si es muy pequeña.

Retraso cero en setTimeout

Hay un caso de uso especial: setTimeout (func, 0), o simplemente setTimeout (func).

Esto planifica la ejecución de func lo antes posible. Pero el planificador lo invocará solo después de que se complete el script que se está ejecutando actualmente.

Por lo tanto, la función está planificada para ejecutarse “justo después” del script actual.

Por ejemplo, esto genera “Hola”, e inmediatamente después “Mundo”:

setTimeout(() => alert("Mundo"));

alert("Hola");

La primera línea “pone la llamada en el calendario después de 0 ms”. Pero el planificador solo “verificará el calendario” una vez que se haya completado el script actual, por lo que “Hola” es primero y “Mundo” después.

También hay casos de uso avanzados relacionados con el navegador y el tiempo de espera cero (zero-delay), que discutiremos en el capítulo Loop de eventos: microtareas y macrotareas.

De hecho, el retraso cero no es cero (en un navegador)

En el navegador, hay una limitación de la frecuencia con la que se pueden ejecutar los temporizadores anidados. EL estándar dinámico de HTML dice: “después de cinco temporizadores anidados, el intervalo debe ser forzado a que el mínimo sea de 4 milisegundos”.

Demostremos lo que significa con el siguiente ejemplo. La llamada setTimeout se planifica a sí misma con cero retraso. Cada llamada recuerda el tiempo real de la anterior en el array times. ¿Cómo son los retrasos reales? Veamos:

let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // recuerda el retraso de la llamada anterior

  if (start + 100 < Date.now()) alert(times); // mostrar los retrasos después de 100 ms
  else setTimeout(run); // de lo contrario replanificar
});

// Un ejemplo de la salida:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100

Los primeros temporizadores se ejecutan inmediatamente (tal como está escrito en la especificación), y luego vemos 9, 15, 20, 24 .... Entra en juego el retraso obligatorio de más de 4 ms entre invocaciones.

Lo mismo sucede si usamos setInterval en lugar de setTimeout: setInterval(f) ejecuta f algunas veces con cero retraso, y luego con 4+ ms de retraso.

Esa limitación proviene de la antigüedad y muchos scripts dependen de ella, por lo que existe por razones históricas.

Para JavaScript del lado del servidor, esa limitación no existe, y existen otras formas de planificar un trabajo asincrónico inmediato, como setImmediate para Node.js. Así que esta nota es específica del navegador.

Resumen

  • Los métodos setTimeout(func, delay, ... args) y setInterval(func, delay, ... args) nos permiten ejecutar func “una vez” y “regularmente” después del retardo delay dado en milisegundos.
  • Para cancelar la ejecución, debemos llamar a clearTimeout / clearInterval con el valor devuelto por setTimeout / setInterval.
  • Las llamadas anidadas setTimeout son una alternativa más flexible a setInterval, lo que nos permite establecer el tiempo entre ejecuciones con mayor precisión.
  • La programación de retardo cero con setTimeout(func, 0)(lo mismo que setTimeout(func)) se usa para programar la llamada “lo antes posible, pero después de que se complete el script actual”.
  • El navegador limita la demora mínima para cinco o más llamadas anidadas de setTimeout o para setInterval (después de la quinta llamada) a 4 ms. Eso es por razones históricas.

Tenga en cuenta que todos los métodos de planificación no garantizan el retraso exacto.

Por ejemplo, el temporizador en el navegador puede ralentizarse por muchas razones:

  • La CPU está sobrecargada.
  • La pestaña del navegador está en modo de “segundo plano”.
  • El portátil está en modo “ahorro de batería”.

Todo eso puede aumentar la resolución mínima del temporizador (el retraso mínimo) a 300 ms o incluso 1000 ms dependiendo de la configuración de rendimiento del navegador y del nivel del sistema operativo.

Tareas

importancia: 5

Escriba una función printNumbers(from, to) que genere un número cada segundo, comenzando desde from y terminando con to.

Haz dos variantes de la solución.

  1. Usando setInterval.
  2. Usando setTimeout anidado.

Usando setInterval:

function printNumbers(from, to) {
  let current = from;

  let timerId = setInterval(function() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }, 1000);
}

// uso:
printNumbers(5, 10);

Usando setTimeout anidado:

function printNumbers(from, to) {
  let current = from;

  setTimeout(function go() {
    alert(current);
    if (current < to) {
      setTimeout(go, 1000);
    }
    current++;
  }, 1000);
}

// uso:
printNumbers(5, 10);

Tenga en cuenta que en ambas soluciones, hay un retraso inicial antes de la primera salida. La función se llama después de 1000ms la primera vez.

Si también queremos que la función se ejecute inmediatamente, entonces podemos agregar una llamada adicional en una línea separada, como esta:

function printNumbers(from, to) {
  let current = from;

  function go() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }

  go();
  let timerId = setInterval(go, 1000);
}

printNumbers(5, 10);
importancia: 5

En el siguiente código hay una llamada programada setTimeout, luego se ejecuta un cálculo pesado que demora más de 100 ms en finalizar.

¿Cuándo se ejecutará la función programada?

  1. Después del bucle.
  2. Antes del bucle.
  3. Al comienzo del bucle.

¿Qué va a mostrar ´alert()´?

let i = 0;

setTimeout(() => alert(i), 100); // ?

// asumimos que el tiempo para ejecutar esta función es > 100 ms
for(let j = 0; j < 100000000; j++) {
  i++;
}

Cualquier setTimeout solo se ejecutará después de que el código actual haya finalizado.

La i será la última:100000000.

let i = 0;

setTimeout(() => alert(i), 100); // 100000000

// asumimos que el tiempo para ejecutar esta función es > 100 ms
for(let j = 0; j < 100000000; j++) {
  i++;
}
Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)