JavaScript — Cómo funciona el Runtime Environment — JRE)
Temario
- Introducción
- Motor de JavaScript
- Single Threaded (síncrono, bloqueante)
- Call Stack
- Heap (memoria)
- JavaScript Runtime Environment (asíncrono, no bloqueante)
- Web APIs
- Callback Queue
- Event loop
- Job Queue
- Entonces… ¿Qué es JavaScript?
i. Introducción
Entender cómo funciona JavaScript por detrás es realmente muy interesante, ya que no solo entendemos como funciona, si no que comprendemos el por que de su comportamiento, mi objetivo es ir explicando cada una de las capas que tiene el ecosistema de JavaScript Runtime Environment (JRE).
Además creo que es un tema que todo desarrollador front-end debe entender (sin importar el framework/librería que estén usando).
Por cierto, todo lo que aprendamos en esta storie, va a servir para las siguientes (Promesas, async/await, generadores, etc), así que… ¡vamos a ello!…
ii. Motor de JavaScript
Anteriormente vimos los diferentes tipos de motores que hay en JavaScript [ref]; podemos decir de manera resumida, que el motor de JavaScript es el encargado de analizar el código, después convertirlo a código máquina y finalmente ejecutarlo.
El motor de JS tiene dos componentes:
- Call stack
- Heap
iii. Call Stack
JavaScript es un lenguaje donde su código se ejecuta en un solo hilo (Single Threaded), esto quiere decir que solo va a suceder una cosa a la vez (puede ser una tarea o fragmento de código) a diferencia de lenguajes como Python o Java que son multihilo (multithread), es decir que pueden ejecutar más de una tarea al mismo instante de tiempo.
Entonces, el motor de JavaScript ejecuta el código de manera ordenada uno a uno, y hasta que termine de ejecutar una tarea o fragmento de código pasa con el siguiente; esto se resume a que el código es síncrono y muchas veces bloqueante (depende GRAN parte de nosotros evitarlo).
Síncrono: Es cuando se ejecuta una operación de entrada/salida de manera secuencial, por lo que debemos de espera a que se complete para poder procesar el resultado.
JavaScript se basa en el sistema LIFO (última entrada, primera salida)[ref], por lo que al ejecutar el código se va ir apilando cada una de las llamadas, (Call Stack).
Veamos el siguiente ejemplo:
Debe verse en consola lo siguiente:
Lo que hace nuestro código:
- Ejecuta console.log(‘Hello world’);
- Después va a ejecutar nuestra función areaRectangle, con los valores 1, 2
- Dentro de la función areaRectangle va a ejecutar la función multiplyTwoNumbers, con los valores x, y
- Dentro de la función multiplyTwoNumbers va a ejecutar console.log(x, y); y va a retornar el valor de la multiplicación.
- Dentro de la función areaRectangle va a ejecutar console.log(r); y va a retornar un valor r.
Por lo que nuestra pila de llamadas (Call Stack), debe verse así:
Había mencionado en la parte de arriba, que el código es…
… síncrono y muchas veces bloqueante (depende de nosotros al momento de escribir código).
Veamos un ejemplo:
Debe verse en consola lo siguiente:
Lo que hace nuestro código:
- Ejecuta la función foo
- Dentro de la función foo va a ejecutar la función foo (repetir este paso infinitamente)
Por lo que nuestra pila de llamadas (Call Stack), debe verse así:
Esto se debe a que excedemos el número de funciones de llamada dentro de nuestro call stack, por lo que se crea un desbordamiento (overflowing), y esto hace que nuestra aplicación se bloquee o deje de funcionar, por eso es muy importante revisar bien nuestro código y evitar posibles bloqueos.
iv. Heap
En el motor de JavaScript tiene otro componente llamado Heap, que es donde se van a almacenar todos los datos dinámicos (definición de funciones, objetos, array, etc), y el componente call stack solo contiene la referencia a ellos.
Todo el ciclo de este componente es manejado por JavaScript, es decir, puede haber en memoria código que ya no utilicemos, pero no nos debemos preocupar tanto por ello, ya que el recolector de basura (garbage collection)[ref] los elimina de manera automática.
En lenguajes de alto nivel (JavaScript), no podemos modificar el algoritmo del recolector de basura, pero si podemos generar buenas prácticas, con el objetivo de optimizar un poco nuestra aplicación; una buena práctica puede ser:
El recolector se dedica a buscar referencias, por lo que podemos aprovechar quitando referencias de los objetos, para que sea considerado como “no necesario” y este lo deseche.
Veamos el siguiente ejemplo[ref]:
Recordemos que por definición, null no tiene prototipo, y actúa como el enlace final de la cadena de prototipos[ref].
Retomemos el ejemplo del área de un rectángulo, y veamos como trabajan los componentes Heap y Call Stack en conjunto:
En el motor de JavaScript debe verse así:
Cuando el navegador carga el archivo JS, el motor de JavaScript empezará a almacenar las definiciones de funciones en memoria (areaRectangle (morado) y multiplyTwoNumbers (azul)) y al momento que el motor ejecuta la función areaRectangle es cuando se agrega al call stack.
¿Sabías que el motor de JavaScript no se ejecuta de forma aislada? Realmente está embebido en el entorno de JavaScript Runtime Environment (JRE), donde existen más componentes y algunos de ellos interactúan con el motor de JS…
v. JavaScript Runtime Environment (JRE)
El JRE es aquel que proporciona características adicionales a nuestra aplicación (ej. click con el mouse, información del navegador, solicitudes HTTP, etc) en el tiempo de ejecución, además es la responsable de hacer que JavaScript sea asíncrono y no bloqueante.
Asíncrono: Ejecutar una operación, donde la finalización es notificada de manera diferida.
Y para hacer uso de la asincronía con JavaScript, vamos a utilizar los famosos callbacks (ya vimos de manera simple en funciones[ref] y que veremos en la próxima story de manera detallada **)
El JRE contiene los siguientes componentes:
- Motor JavaScript
- Web APIs
- Callback Queue (cola de devolución o cola de mensajes)
- Job Queue
- Event Loop (bucle de eventos)
vi. Web APIs
Son APIs que nos proporciona el navegador, si bien no son parte del motor de JavaScript, pero si del entorno de tiempo de ejecución, esto quiere decir que el navegador nos expone las APIs y JavaScript nos facilita su acceso, esto con el fin de poder hacer interacciones dentro de nuestra aplicación (ej. escuchar algún evento (click mouse), seleccionar un nodo DOM del HTML, hacer peticiones a servicios (XMLHttpRequest), etc…).
Cuando escribimos código para la web utilizando JavaScript, podemos usar gran número APIs disponibles [ref]
Cuando el motor de JavaScript encuentra una web API, este va a ser enviado a una tabla de eventos, donde va a esperar hasta que ocurra el evento o haga una devolución de llamada.
Veamos el siguiente ejemplo (no olvides darle play para ejecutar):
En este ejemplo vamos a usar el document (querySelector y addEventListener), no te preocupes por entenderlos más adelante vamos a verlo con mucho detalle (JavaScript — Document(DOM)[ref]), por ahora solo debemos enfocarnos en cómo funciona el JRE.
En nuestro JRE debe verse así:
Podemos observar en la imagen que:
- Ejecuta console.log(‘Hello world’);
- Después se ejecuta el document.querySelector… El motor de JavaScript detecta que es Web API (querySelector, addEventListener).
- Entonces lo que hace es guardarla en la tabla de los eventos (web APIs)
- Ejecuta console.log(‘Goodbye!!’);
vii. Callback Queue
Las APIs cuando son requeridas (puede ser un evento como click, o una recuperación de respuesta como callback), se van apilando en el componente callback queue. Utiliza el modelo de FIFO (primera entrada, primera salida)[ref], por lo que mantiene el orden en que fueron agregadas las tareas, y finalmente, es ejecutado en el componente del motor de JavaScript (Call stack).
Nota: Para una mejor visualización en los ejemplos, he simplificado la parte del motor de JavaScript
Continuando con nuestro ejemplo, qué pasa si damos click en el botón, en nuestro JRE debe verse así:
Podemos observar que:
- Al darle click al botón la API se activa y manda el evento ([Event](click)) al Callback Queue
- El Callback Queue le pasa el evento al motor de JavaScript
- Se ejecuta [Event](click)
- Se ejecuta console.log(‘You clicked the button!’);
Veamos un ejemplo un poco más complejo (no olvides darle play para ejecutar):
En consola quizás esperes ver lo siguiente:
"Hello!"
"Hello from the setTimeOut0 => 0ms"
"Hello from the setTimeOut3000 => 3000ms"
"Hello for!!" 0
"Hello for!!" 1
"Hello for!!" 2
"Hello for!!" 3
"Hello for!!" 4
"Hello for!!" 5
"Goodbye!!"
Pero no, el resultado que sale en consola es el siguiente:
Podemos observar que la consola del método setTimeOut0 y setTimeOut3000 se imprimen al final; no hay que asustarnos, realmente el “culpable” de que salga en ese orden, es el componente Event Loop…
viii. Event loop
El componente Event loop tiene dos funciones principales:
1 — Comprobar continuamente si el motor de JavaScript en el contexto de ejecución (Call stack) tiene tareas, en caso SI tener tareas vuelve a comprobar.
2 — Cuando el motor de JS NO tiene tareas, va a comprobar si hay eventos (cola de mensajes) en el Callback Queue, en caso de que tenga los manda al motor de JS a que los ejecute.
Veamos la siguiente animación para entender cómo funciona nuestro ejemplo:
Podemos observar en la animación que:
- Ejecuta la última tarea: console.log(‘Goodbye!!’);
- Event loop comprueba que ya NO se tenga más tareas en la pila del call stack, entonces…
- Event loop comprueba, SI tiene eventos en la callback queue, entonces…
- Pasa el evento al motor de JS y este ejecuta función timeout0
- Ejecuta console.log(‘Hello from the setTimeOut => 0ms’);
- Pasa el evento al motor de JS y este ejecuta función timeout3000
- Ejecuta console.log(‘Hello from the setTimeOut => 3000ms’);
Importante: El paso de callback queue a el motor de JavaScript, lo hace uno a uno, esto quiere decir que lo hace de manera síncrona, por lo que debemos cuidar que el código ejecutado en el motor no sea bloqueante.
Veamos un ejemplo aún más complejo:
En este ejemplo vamos a usar promesas (Promise), no te preocupes por entenderlos más adelante vamos a verlo con mucho detalle (JavaScript — Callbacks y Promises (Básico)[ref]), por ahora solo debemos enfocarnos en cómo funciona el JRE.
En consola quizás esperes ver lo siguiente:
"Hello!"
"Hello for!!" 0
"Hello for!!" 1
"Hello for!!" 2
"Hello for!!" 3
"Hello for!!" 4
"Hello for!!" 5
"Goodbye!!"
"Hello from the setTimeOut0 => 0ms"
"Hello from the setTimeOut3000 => 3000ms"
"Hello from the Promise => 1st"
"Hello from the Promise => 2nd"
Pero no, el resultado que sale en consola es el siguiente:
Podemos observar que primero van las promesas, y al final los setTimeOut; no hay que asustarnos, realmente el “culpable” de que salga en ese orden, es el componente Job Queue…
ix. Job Queue
En ES2015 se añadió el componente Job Queue, donde es utilizado por las promesas.
Cuando el motor de JavaScript no tiene tareas, el event loop le va a dar prioridad al componente de Job queue (al resultado de funciones asíncronas) y después al callback queue.
Las promesas que se resuelvan antes de que finalice la función actual se ejecutarán justo después de la función actual.
xi. Entonces… ¿Qué es JavaScript?
JavaScript es un lenguaje concurrente, donde su código se ejecuta en un solo hilo, esto quiere decir que solo va a suceder una cosa a la vez (puede ser una tarea o fragmento de código), donde usa el modelo asíncrono (gracias a las Web APIs)
Nota: Para ver cómo funciona JRE de JavaScript en tiempo real, he ocupado la herramienta Loupe [ref].
Más adelante veremos cómo hacer Multithreading con Javascript (Web Workers)…
En la siguiente entrega vamos a ver JavaScript — Callbacks y Promises
La entrega pasada vimos JavaScript —ES (Parte I)
Bibliografía y links que te puede interesar…