Una función sleep()
es una función que permite detener la ejecución del código durante un tiempo determinado. Usar una función similar a esta puede ser interesante para múltiples cuestiones: desde esperar a que se cumpla alguna condición antes de continuar con el código, hasta simular durante el desarrollo una conexión asíncrona que tarda mucho en devolver algo.
Casi todos los lenguajes y plataformas tienen algún modo de hacer esto, pero JavaScript, no dispone de esta funcionalidad de forma nativa. En este artículo vamos a ver cómo implementar una función sleep()
en JavaScript usando dos métodos diferentes: el clásico y el moderno, explicando cómo lograrlo y cuáles son sus ventajas y desventajas.
sleep() con JavaScript "clásico" (ECMASCript 5)
En los tiempos de ECMAScript 5 (es decir, hasta antes de 2015) la única manera de simular una función sleep()
era ejecutar un bucle a lo loco durante el tiempo que nos interesase "detener" la ejecución del código. La implementación de una función sleep()
sería algo similar a esto:
var sleepES5 = function(ms){
var esperarHasta = new Date().getTime() + ms;
while(new Date().getTime() < esperarHasta) continue;
};
Como ves, lo que hace es sumar el número de milisegundos que se le pasen como parámetro y ejecutar un bucle sin hacer nada hasta que pase el tiempo estipulado. Más simple imposible.
Luego se podía meter en un fragmento de código para detenerlo durante un rato escribiendo algo así:
function pruebaES5(){
console.log('Inicio de la función de prueba.');
sleepES5(3000); //Dormimos la ejecución durante 3 segundos
console.log('Fin de la función de prueba.');
};
Esto cumple con su cometido, y nos puede valer para hacer pruebas en desarrollo y cosas así, pero tiene varias pegas:
- La ejecución realmente no se detiene, ya que lo que se hace es ejecutar un bucle miles de veces mientras esperamos. Por lo tanto se está utilizando CPU y, de hecho, si se pone un tiempo más o menos largo (unos cuantos segundos, dependiendo del navegador), acabaremos recibiendo un mensaje de aviso para detener la ejecución.
- La interfaz de usuario se bloquea. Dado que JavaScript solo tiene un único hilo de ejecución, no puede ejecutar dos tareas a la vez (y el código no está en un Web Worker ni en un Service Worker), por lo que durante la espera la UI deja de responder. En realidad, los clics y acciones que lleves a cabo se encolan y se ejecutan cuando termina la ejecución del código "desbocado".
Si en nuestro ejemplo (que podrás descargarte al final) colocamos un botón que puedas pulsar durante la espera para hacer otra cosa, no hará caso hasta que la ejecución finalice.
En la siguiente figura se puede ver el resultado de ejecutar el código anterior (con los mensajes formateados para que se vean mejor), pulsando otro botón para hacer otra cosa durante el "sleep":
Fíjate en que la ejecución se detiene 3 segundos, pero las pulsaciones del botón solamente se ejecutan al terminar la espera. Es decir, la espera no es tal, sino que es un bloqueo de la ejecución durante el tiempo indicado.
Es más, si mostramos el gestor de tareas del navegador y nos fijamos en el uso de CPU de la página durante el tiempo de espera, veremos que llega a picos muy altos de uso (que dependen de la potencia de tu máquina):
Como digo, es una "ñapa" que funciona, pero que no es un sleep()
de verdad y que presenta estos graves problemas. Pero, oye, en JavaScript "clásico", antes de ES6, era la única solución a nuestro alcance.
sleep() con navegadores modernos (ES6)
Una de las grandes y esperadísimas novedades cuando salió ECMASCript 6 era la incorporación al lenguaje de la característica de promesas (Promise
).
Una promesa es un objeto que representa una tarea que "promete" ejecutarse en algún momento determinado (en el futuro o, incluso, en el pasado, aunque parezca un contrasentido), y son el mecanismo nativo de ECMAScript para ejecutar código asíncronamente.
Como las promesas tienen su complejidad, también se incorporaron al lenguaje las palabras clave async
y await
, para simplificar el manejo de funciones asíncronas con promesas.
Vamos a aprovechar esta funcionalidad para crear una función sleep()
que de verdad funcione como esperamos.
Hacerlo, en realidad, es de lo más sencillo ya que tan solo necesitamos devolver una promesa y que ésta se resuelva automáticamente al cabo del tiempo indicado. O sea, el código es el siguiente:
var sleep = function(ms){
return new Promise(resolve => setTimeout(resolve, ms));
};
Se crea la promesa y se indica como función de resolución un timeout que se ejecuta al cabo del tiempo indicado. La función del timeout tiene acceso al valor de la variable ms
gracias a la clausura de la función.
¡Más fácil imposible!
Si no dominas las promesas, ni la programación asíncrona, ni las clausuras... te estás perdiendo mucho. Demasiado. Un desarrollador JavaScript que se precie debe dominar todo esto y muchas otras cuestiones avanzadas. Échale un vistazo a esta formación.
Vale, vamos a ver cómo se ejecutaría esto ahora en nuestro código:
async function pruebaES6(){
console.log('Inicio de la función de prueba.');
await sleep(3000); //Dormimos la ejecución durante 3 segundos
console.log('Fin de la función de prueba.');
};
Fíjate en que la función que llame a nuestra función sleep()
, como cualquier función que llame a un método asíncrono, debe llevar un modificador async
delante. Y la llamada debe ir precedida de un await
.
Durante las espera de 3 segundos hemos pulsado el botón de enviar mensajes por consola 3 veces, al igual que en el ejemplo anterior. El resultado en la consola es el siguiente:
Si te fijas bien y lo comparas con la ejecución del código "tradicional" verás que, aparte de ejecutarse la espera, ahora la interfaz de usuario no se bloquea y cuando pulsamos el otro botón los mensajes llegan a la consola en el mismo momento en el que lo hacemos. Es decir, este sleep()
sí que detiene de verdad la ejecución de nuestro código y además no bloquea el hilo principal del navegador y por lo tanto todo es interactivo y alejamos el fantasma del mensaje de "Un script está tardando mucho tiempo en ejecutarse".
Y ¿qué pasa con el uso de procesador?
Pues durante todo el tiempo en que se detienen la ejecución el uso de la CPU es del 0%:
Así que ¡perfecto! hemos conseguido un sleep()
de verdad, que funciona como cabría esperar y sin desventajas por hacerlo.
Te dejo el código de prueba (1,22Kb) que he usado para este artículo para que puedas jugar con él.
¡Espero que te resulte útil!