Javascript. Event Loop y Promises

Cómo funciona el Event Loop de Javascript a la hora de ejecutar código asíncrono

Image for post
Image for post

English version: https://medium.com/@ger86/javascript-event-loop-y-promises-951ba6845899

Hoy he preparado uno de esos artículos que creo que sirven para reforzar ciertos conceptos de Javascript por los que muchas veces pasamos de largo ya que el día a día nos obliga a centrarnos más en la resolución de problemas que en entender la forma en que las cosas funcionan por debajo.

Concretamente hoy quiero hablar acerca de las Promises, ese nuevo tipo de objeto que apareció (por fin) de forma nativa con la llegada de ES6 y que nos permite realizar tareas de forma asíncrona para, una vez completadas, obtener el resultado de las mismas (o un fallo si se produjo) y ejecutar el código que necesitemos. Pero entonces… ¿es Javascript asíncrono? Bien, para responder a esta pregunta también profundizaremos en la “herramienta” conocida como Event Loop y que tan a menudo aparece en las preguntas que los técnicos de selección hacen en las entrevistas (el año pasado en cada entrevista a la que fui tuve que explicarlo). Así que, ¡comencemos!

Sí, Javascript es síncrono

Partamos de la base de que Javascript es síncrono y que posee un único hilo de ejecución. Es decir, sólo es capaz de ejecutar una tarea a la vez la cual bloquea la ejecución hasta que es terminada y puede pasar a la siguiente.

Sin embargo… ¿qué sucede cuando tenemos que ejecutar una consulta pesada? Por ejemplo, obtener una lista de 10.000 registros con los que posteriormente trabajaremos. ¿No hay forma de evitar que se bloquee la ejecución de modo que mientras podamos seguir haciendo otras tareas? Es aquí donde aparece la solución que propuso por la comunidad de desarrolladores con el fin de dotar a Javascript de cierto aspecto de asincronicidad: el famoso Event Loop.

El Event Loop de Javascript

Para enteder el Event Loop de Javascript, primero veamos como funciona una pila de ejecución síncrona:

Image for post
Image for post

Como veis, cuando la función a() comienza a ejecutarse, se van añadiendo a la pila las sucesivas llamadas al resto de funciones las cuales se van ejecutando y eliminándose de la pila conforme terminan.

Ahora bien, imaginad que tenemos el siguiente código:

Image for post
Image for post

En este caso, si estuviéramos trabajando en una pila de ejecución síncrona la función setTimeout provocaría que la ejecución se detuviese 10 segundos (bloqueando por tanto el programa) por lo que no habría manera de realizar nada más mientras esperamos a que el contador finalice.

Para resolver este tipo de situaciones es para lo que se implementó el conocido Event Loop, el cual permite que la ejecución de ese tipo de tareas sea realizada de forma asícrona de modo que:

  • la ejecución no es bloqueada
  • una vez que la tarea asíncrona ha sido finalizada se ejecute su callback cuando sea posible (esto último es muy importante tenerlo en mente)

Veamos cómo funciona:

Image for post
Image for post
  • En el paso 2, cuando la función setTimeout(callback, 10000) es puesta en la pila, esta llamada es pasada a la Web API del navegador por lo que ya no pertenece al motor de Javascript, sino a una característica adicional que proporciona el navegador (o el sistema donde se ejecuta).
  • Por tanto, en el paso 3 se puede ver como es la Web API quien toma la responsabilidad de que la función callback se ejecute.
  • En el paso 4 podemos ver como el otro console.log es ejecutado de modo que en el paso 5 la pila ya se encuentra vacía.
  • El paso 6 tiene lugar una vez que han pasado los 10 segundos del setTimeout que se encuentra en la Web API (y que la pila de ejecución del moto de Javascript se encuentra vacía). Puesto que la Web API no puede añadir directamente nada en la pila (podría provocar la interrupción de código que se esté ejecutando en ese momento), lo que hace es añadir el callback a la Callback queue (paso 7).
  • Es en el paso 8 donde el Event Loop entra en acción. En el momento en el que la pila del motor de Javascript se encuentra vacía el Event Loop va cogiendo aquello que se encuentre en la callback queue y lo añade a la pila de ejecución.
  • A partir de ahí, la ejecución del callback sigue el proceso de ejecución normal (pasos 10 a 13) hasta queda la pila vacía.

Por tanto, pese a que Javascript no sea asíncrono la inclusión de la WebAPI junto con el Event Loop y la Queue Callback permiten dotarle de cierto aspecto de asicronicidad de modo que las tareas más pesadas no bloqueen el hilo de ejecución.

Sin embargo, existe un pero. Puesto que el callback de la función no se sabe en qué momento se ejecutará (ya que como hemos visto es necesario que la pila se quede vacía para que el Event Loop pueda añadir cosas a ella procedentes de la queue callback) es posible que sea necesario anidar sucesivas llamadas dentro del callback de nuestra función, algo que se conoce como el callback hell. Es para solucionar esto para lo que surgieron las Promises como veremos a continuación.

Image for post
Image for post

Promises

Con el fin de evitar este callback hell se desarrollaron una serie de librerías como Bluebird o Q que permitían limpiar un poco toda esa maraña de funciones anidadas y escribir código que operaba de forma asíncrona pero que parecía escrito como si fuera síncrono: habían nacido las Promises.

Posteriormente, con la llegada de ES6, la propuesta para llevar las Promises a Javascript de forma nativa fue aceptada quedando su sintaxis del siguiente modo:

const p = new Promise(function(resolve, reject) {
return setTimeout(function() {
resolve(1);
}, 10000);
});
p.then(function(value) {
console.log(value)
});
// 1

Podemos definir una Promise como un objeto que puede produce un único valor en algún momento del futuro, bien sea un valor resulto bien la razón por la que no pudo resolverse.

Una Promise puede estar en tres estados: pendiente, fulfilled o rejected. A estos objetos Promise los desarrolladores podemos adjuntarles callbacks mediante la instrucción then de modo que podamos ejecutar código una vez que se disponga del valor resuelto por la Promise (o la razón por la que no pudo resolverse).

Además, otra característica de las Promises es que son eager (no encuentro un término adecuado en español para esta palabra que se adecúe al sentido que tiene en este contexto), es decir, una promise comenzará a hacer la tarea que reside en su cuerpo tan pronto como el constructor es invocado. Podéis verlo en este ejemplo:

http://latentflip.com/loupe/?code=Y29uc3QgY2FsbGJhY2tQcm9taXNlID0gZnVuY3Rpb24odmFsdWUpIHsKICAgIGNvbnNvbGUubG9nKCdjYWxsYmFjayBwcm9taXNlJyk7Cn0KY29uc3QgcCA9IG5ldyBQcm9taXNlKGZ1bmN0aW9uKHJlc29sdmUpIHsKICAgIHJldHVybiBzZXRUaW1lb3V0KHJlc29sdmUsIDEwMDAwKTsKfSk7CmNvbnNvbGUubG9nKCdDb21pZW56bycpOwpwLnRoZW4oY2FsbGJhY2tQcm9taXNlKTsKY29uc29sZS5sb2coJ0ZpbicpOw%3D%3D!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D

Si habéis ejecutado el código veréis como la llamada a setTimeout es lo primero que se ejecuta, alojando dicha instrucción en las Web Apis.

Promises y Event Loop

Sin embargo, la ejecución de las Promises y sus callbacks es algo distinta a la de los callbacks asíncronos que vimos en el punto anterior pues los callbacks de las Promises son añadidos a una nueva cola que no habíamos comentado antes: la microtask queue.

A partir de este momento diferenciaremos entre dos tipos de tareas asíncronas:

  • las macrotasks, las cuales son programadas de modo que el navegador pueda garantizar que se ejecutan de manera secuencial al acceder desde su motor interno al de Javascript. Por ejemplo son consideradas macrotasks los callbacks de los eventos de navegador (un onClick ) o los de las funciones como setTimeout que vimos antes.
  • las microtasks, las cuales son programadas para cosas que deberían suceder inmediatamente después de la secuencia de comandos que se está ejecutando actualmente como por ejemplo la realización de algo asíncrono sin soportar la penalización de crear una nueva macrotask. Estas microtasks son encoladas en la microtask queue la cual es procesada después de la de macrotasks y al final de la ejecución de cada macrotask siempre que no haya Javascript ejecutándose. Entre las microtask se encuentran, como comentaba antes, los callbacks de los objetos Promise .

De este modo, en una iteración del Event Loop tendremos:

  1. Primero se comprueba si hay alguna tarea disponible en la cola de macrotasks.
  2. Si es así y dicha tarea se está ejecutando, esperar hasta que se complete antes de ir al siguiente paso. Si no, ir directamente al paso 3.
  3. A continuación, ejecutar todas las microtasks que se encuentren en la microtask queue.
  4. En el caso de que durante la ejecución de las microtasks añadamos nuevas microtasks, estas también son ejecutadas.

Como corolario de esta secuencia podríamos decir que dos macrotasks no pueden ejecutarse una detrás de la otra si entre medias la cola de microtasks tiene elementos.

Por tanto, en el momento en que definimos la resolución de una promesa (mediante la instrucción then ) se encola una nueva microtask que representa dicho callback. Esto nos permite asegurar que dichos callbacks sean asíncronos incluso cuando la promise ya haya sido resuelta. Es decir, si tenemos el siguiente código:

console.log('start');  
Promise.resolve()
.then(function() {
console.log('promise 1');
})
.then(function() {
console.log('promise 2');
});
console.log('end');
// start
// end
// promise 1
// promise 2

Los console.log pertenecientes al callback de la promise definida se pintan al final puesto que se encolan pese a que estemos resolviendo la promise de forma asíncrona.

En este otro ejemplo:

console.log('start');  setTimeout(function() {   
console.log('timeout finished');
}, 0);
Promise.resolve()
.then(function() {
console.log('promise 1');
})
.then(function() {
console.log('promise 2');
});
console.log('end');
// start
// end
// promise 1
// promise 2
// timeout finished

podéis ver como el timeout finished es escrito al final del todo, pues el script en sí mismo es tratado como una macrotask de modo que al finalizarse se ejecutan las microtasks encoladas, es decir, los dos console.log referentes a los callbacks de las promesas.

Conclusiones

Como habréis visto, hay muchos conceptos detrás de las Promises pese a la regularidad con la que las usamos y lo omnipresentes que se encuentran ya en cualquier código Javascript actual.

Por tanto, espero que este artículo os haya servido para repasar o descubrir todo este tipo de conceptos. Y como siempre, Si os habéis quedado con alguna duda o queréis aportar algo a este artículo dejadlo en los comentarios, ¡estaré encantado de escucharos!

¿Quieres recibir más artículos como este?

Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store