👉 Ver todas las notas
JavaScript es un lenguaje de programación single-thread, lo que equivale a decir que sólo puede hacer 1 cosa a la vez, ejecutar 1 sola instrucción y finalizarla antes de pasar a la siguiente.
Tener 1 sólo thread de ejecución significa tener también 1 sólo stack, por lo que las operaciones lentas (como el procesamiento de imágenes o los requests HTTP) resultan bloqueantes (bloquean el thread de ejecución), en el sentido de que el resto de las instrucciones de nuestro código no se ejecutarán hasta que estas finalicen.
Nota: llamamos operaciones bloqueantes (o blocking) a aquellas que son lentas, de las que no podemos obtener un resultado de forma inmediata
Si tenemos muchas operaciones bloqueantes, vemos claramente el gran impacto que esto tendría en la performance de nuestra aplicación. Un browser por ejemplo, no podría realizar ciertas operaciones como renderizar la UI correspondiente, resultando en una experiencia de uso poco deseable.
Ver What the heck is the event loop anyway? | Philip Roberts | JSConf EU
👉 La forma que tenemos de evitar bloquear nuestra aplicación es escribiendo código asincrónico, utilizando callbacks o Promises.
Como mencionamos antes, JavaScript es single-thread, por lo que no puede ejecutar más de 1 tarea (proceso) a la vez. Esto es cierto, pero la plataforma (o entorno) sobre la que corremos JavaScript si permite realizar más tareas, de forma concurrente. Por ejemplo, a través del browser tenemos acceso a las Web APIs, que nos proveen de más threads para realizar ciertas tareas en un 2do plano, es decir, fuera del thread principal. Algo similar ocurre en Node.
👉 Cuando estas APIs externas terminan de realizar la tarea asignada, la envían a una cola de tareas (callback queue). Es en este momento cuando aparece el event loop para realizar una tarea muy simple: encargarse de chequear el stack de funciones actual y el callback queue; si el stack se encuentra vacío, toma el primer callback1 (del callback queue) y lo pushea al stack para que sea ejecutado.
Las tareas asincrónicas se delegan a APIs externas (threads adicionales) y luego son encoladas (en el callback queue) para eventualmente ejecutarse en el thread principal.
👉 Es importante notar que el Event Loop no forma parte de JavaScript en si, sino del entorno donde este se ejecute (browser, Node, etc).
El concepto de Event Loop resulta entonces, bastante simple. Se trata de un loop infinito que espera a que el thread principal esté libre y haya tareas disponibles esperando, para asignarle una nueva tarea, proveniente del callback queue, para luego quedarse esperando hasta que haya más tareas.
El rendering en el browser nunca sucede mientras el runtime de JavaScript (engine) se encuentra ejecutando una tarea, independientemente de si una tarea toma o no mucho tiempo. Los cambios que realicemos en el DOM no se verán reflejados hasta que la tarea finalice.
Por lo tanto si una tarea toma mucho tiempo, estamos bloqueando la UI (y el thread) y el browser no puede ocpuarse de otras cosas (como procesar eventos). Luego de cierto tiempo, dependiendo de cada browser, mostrará un mensaje indicando que la página no responde, por lo que necesitaremos cerrar la pestaña, el browser o terminar de alguna forma el proceso.
👉 Es por esto que debemos evitar realizar tareas muy complejas (computacionalmente costosas) en el thread principal (o que tomen mucho tiempo) si queremos evitar una mala UX.
A su vez, las tareas asincrónicas pueden dividirse en macro y micro tareas:
- macrotasks: como
SetInterval
oSetTimeout
, se ejecutan en el siguiente event loop, es decir, la próxima iteración. - microtasks: como una Promise resuelta, se ejecutan antes del inicio del próximo event loop, es decir, tienen prioridad sobre las macrotasks y se van a ejecutar antes. Los mismo sucede con Async/Await, al tratarse de otra forma de escribir Promises.
Podríamos decir entonces, que en realidad el callback queue está compuesto por 2 colas más pequeñas: microtask queue y macrotask queue y que el Event Loop se va a encargar de asignar al thread principal, primero todas las microtasks y a continuación, cuando todas estas se completen, las macrotasks.
👉 Inmediatamente después de cada macrotarea, el engine ejecuta todas las tareas de la cola de microtareas, antes de cualquier otra macrotask, rendering, etc. Las microtasks siempre tienen prioridad sobre el resto de las tareas asincrónicas.
Event Loop y async/await
- Escribir código asincrónico (ver callbacks, Promises, Async/Await)
- Evitar realizar operaciones computacionalmente costosas (a nivel CPU) en el stack para no bloquear el event loop (como procesamiento de imágenes o video)
- Dividir tareas costosas en tareas más chicas, aprovechando el asincronismo
Para realizar cálculos complejos u operaciones muy largas y evitar bloquear el event loop, lo más conveniente es utilizar Web Workers.
Básicamente, nos permiten ejecutar código en otro thread, fuera del principal.
👉 Web Workers pueden intercambiar información con el thread principal, pero tienen sus propias variables e incluso su propio event loop.
Los Web Workers no tienen acceso al DOM, por lo que suelen utilizarase para realizar cálculos complejos y aprovechar los múltiples cores del CPU para poder ejecutar código de forma paralela, a diferencia de la concurrencia que nos provee el asincronismo.
1Hay tareas del callback queue que tienen prioridad sobre otras y por lo tanto el event loop las moverá antes al thread principal. Ver macrotasks & microtasks.