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);
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);
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.
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.
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)
ysetInterval(func, delay, ... args)
nos permiten ejecutarfunc
“una vez” y “regularmente” después del retardodelay
dado en milisegundos. - Para cancelar la ejecución, debemos llamar a
clearTimeout / clearInterval
con el valor devuelto porsetTimeout / setInterval
. - Las llamadas anidadas
setTimeout
son una alternativa más flexible asetInterval
, 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 quesetTimeout(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 parasetInterval
(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.