Javascript y su asincronía.

Como realiza las tareas mediante el Event loop.

Anderson Sánchez
Academia Hack
7 min readAug 10, 2019

--

Javascript es un lenguaje de programación asíncrono, no bloqueante y de un solo hilo. Posee un call stack(pila de llamadas), utiliza un event loop(ciclo de eventos), un callback queue(cola de callbacks) y unas APIS para su funcionamiento.

Si, no te preocupes, no solo te ocurre a ti. La primera vez que escuché acerca de estos términos, no tenia ni idea de lo que significaban. En este artículo vamos a ver de que se tratan, y por que son tan importantes hoy en día en el desarrollo web.

Primero veamos un diagrama que representa el Runtime de V8 Javascript (utilizado por Chrome y Node).

Diagram by: https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

Dicho Runtime está compuesto por dos elementos principales:

  • Memory heap(montón de memoria): Locación de memoria.
  • Call stack(Pila de llamadas): Con esta pila de llamadas el Runtime mantiene un seguimiento de las llamadas a las funciones.

A nosotros como desarrolladores, principalmente nos interesa el call stack. Una llamada se refiere a la invocación de un método/función.

Recordemos que una estructura de pila, es aquella que tiene la característica, que el elemento que ingresa primero, sale de ultimo. (Se van apilando uno encima de otro).

Volviendo al call stack de Javascript, este realiza un proceso simple, cuando se está a punto de ejecutar una función se añade al stack. Si dicha función llama a su vez a otra función, es agregada sobre la anterior.

Javascript es single threaded

Esto quiere decir que durante la ejecución de un script existe un solo thread(hilo) que ejecuta el código. Por lo tanto solo se cuenta con un call stack. Veamos un ejemplo:

Al invocar la función multiplicar(), dicha llamada se agrega al call stack y es ejecutada.

Ahora, veamos que ocurre cuando invocamos a la función calcularPotencia()

En el diagrama podemos observar el orden de ejecución.

  • Se añade la invocación de calcularPotencia().
  • calcularPotencia invoca a la función multiplicar(), por lo que esta se añade a la pila.
  • Se ejecuta la función multiplicar(), por lo que sale de la pila.
  • Se añade a la pila el console.log() invocado en calcularPotencia.
  • Se ejecuta el console.log(), por lo que sale de la pila.
  • Se finaliza la ejecución de calcularPotencia(), por lo que sale de la pila.
  • La pila se vacía.

En conclusión, Javascript va ejecutando una llamada(invocación) a la vez. Ese es el comportamiento de un solo hilo.

Seguimiento de la ejecución del stack

Si en algún momento de la ejecución hay un error, este se imprimirá en la consola con un mensaje y el estado del call stack al momento en que ocurrió.

Por ejemplo, al realizar este código:

Nos muestra como fue ejecutándose el código definido, siguiendo una traza.

¿Qué ocurre si la pila se llena?

Ejecutemos este código, una invocación infinita de la función foo, sin condición de salida.

Lo que sucedería es que en algún momento la cantidad de funciones llamadas excede el tamaño del stack , por lo que el navegador mostrará este error:

Se llenó la pila de llamadas, y el navegador entiende que no se dejará de invocar. Por lo que detiene la ejecución y muestra el error.

Pero qué pasa si llamamos a un setTimeout o hacemos un request con AJAX a un servidor. Al ser un solo thread, hay un solo call stack y por lo tanto solo se puede ejecutar una cosa a la vez. Es decir el navegador debería congelarse, no podría hacer más nada, no podría renderizar, hasta que la llamada termine de ejecutarse. Sin embargo esto no es así, javascript es asincrónico y no bloqueante. Esto es gracias al Event Loop.

Event Loop

Algo interesante acerca de javascript, o mejor dicho de los runtimes de javascript, es que no cuentan nativamente con cosas como setTimeout, DOM, o HTTP request. Estas son llamadas web APIS, que el mismo navegador provee, pero no están dentro del runtime JS.

Por lo tanto este es el gráfico que muestra una visión más abarcativa de javascript. En este se puede ver el runtime, más las Web APIS y el callback queue del cual hablaremos más adelante.

Función setTimeout

El método setTimeout() invoca una función o evalúa una expresión después de un numero específico de mili segundos.

En este ejemplo, se ejecutará un mensaje de alerta luego de 3 segundos de espera:

Veamos un poco mas:

¿En que orden se van a mostrar los mensajes?.

Siguiendo con el ejemplo de ejecución en el callstack:

  • invoca() se añade al call stack
  • invoca() llama a finish() por lo que se añade al call stack y se esperan 5 segundos para ejecutarse.
  • Al ejecutar finish(), se añade el console.log(‘terminé’) al call stack y se ejecuta.
  • Luego desde invoca() se llama a hola(), sin embargo se esperan 2 segundos para ejecutarse.
  • Al ejecutar hola(), se añade el console.log(‘hola) al call stack y se ejecuta
  • Por último, se ejecuta el console.log(‘aún no he terminado’)
  • La pila se vacía.

Sin embargo, en realidad esto no ocurre así, como dije anteriormente javascript es asincrónico y no bloqueante. Esto es gracias al Event Loop, al ver el resultado obtenemos:

Al ser un solo thread, hay un solo call stack y por lo tanto solo se puede ejecutar una cosa a la vez. Es decir el navegador debería congelarse cada vez que exista un timer, no podría hacer más nada, no podría renderizar, hasta que la llamada termine de ejecutarse.

¿Que pasaría si a un usuario que interactuando con nuestra página le sucediera esto?

Lo más probable es que cierre el navegador y nunca más vuelva a entrar a nuestra página. No es algo que queremos que suceda.

Javascript es asíncrono y no bloqueante.

Para evitar este comportamiento, si el call stack recibe la llamada que una función debe ejecutarse luego de un timer, la quita del stack momentáneamente y continua ejecutando la siguiente tarea.

Al haber un solo thread es importante no escribir código bloqueante para la UI no quede bloqueada.

Pero ¿Cómo hacemos para escribir código no bloqueante?

La solución son callbacks asincronicas. Para esto combinamos el uso de callbacks (funciones que pasamos como parámetros a otras funciones ) con las WEB API’s. Para utilizarlas debemos entender la diferencia entre estos dos conceptos:

  • Función de orden primario: Cualquier función que tome otra función como parámetro o retorne una
  • Callbacks: Se le llama callback a una función que sea pasada como parámetro.

Veamos otro ejemplo:

Como pueden ver ocurrió lo mismo del ejemplo anterior. Esto es así por que el setTimeout NO es parte del runtime. Sino que es provista por el navegador como WEB APIs ( o en el caso de Node por c++ apis). Los cuales SI se ejecutan en un thread distinto.

¿Como se maneja esto con una única call stack?

Existe otra estructura donde se guardan las funciones que deben ser ejecutadas luego de cierto evento (timeout, click, mouse move), en el caso del código de ejemplo de arriba se guarda que, cuando el timeout termine se debe ejecutar la función timeoutCallback(). Tener en cuenta que cuando sucede el evento, esta estructura no es la que la ejecuta y tampoco las agrega al call stack ya que sino podría pasar que la función se ejecutará en medio de otro código. Lo que hace es enviarla a la Callback Queue.

Lo que hace el event loop es fijarse el call stack, y si está vacío (es decir no hay nada ejecutándose) envía la primera función que esté en la callback queue al call stack y comienza a ejecutarse.

Luego de terminar la cuenta regresiva del setTimeout() (que no es ejecutada en el runtime de javascript), timeoutCallback() será enviada a la callback queue. El event loop chequeara el Call Stack, si este está vacío enviará timeoutCallback() al call stack para su ejecución.

Mejor entendamos todo este trabalenguas con un gif.

¿Por que si bloqueamos el call stack la UI ya no responde más?

Lo que ocurre es que el navegador intenta renderizar la vista cada cierto tiempo. Sin embargo, si el call stack no está vacío, no puede realizarse. Debe esperar a que no haya nada en la pila para poder ejecutarse. Esto quiere decir que si hay código bloqueante, el proceso de renderizado, tardará mas en realizarse y el usuario no podrá hacer nada.

Para finalizar, puedes ver este vídeo, donde Philip Roberts explica muy bien el comportamiento del runtime de Javascript.

--

--