Async JS bajo el capó

Jan 27 2023
MDN define el flujo de trabajo asíncrono como ..

MDN define el flujo de trabajo asíncrono como

..iniciar una tarea potencialmente de larga ejecución y aún así poder responder a otros eventos mientras se ejecuta esa tarea... Una vez que la tarea ha terminado, se le presenta el resultado a su programa.

Pero, ¿cómo se puede lograr esto exactamente en la web con JavaScript?

Antes de sumergirse en las implementaciones asíncronas, es útil tener una comprensión básica de JS en un nivel inferior.

¿Cómo funciona JS?

JavaScript es de subproceso único e interpretado, lo que significa que las instrucciones se ejecutan directamente y no tienen un paso de compilación ex ante. Tomemos como ejemplo el V8, el motor más popular.

En un alto nivel, V8 analiza el código, crea un árbol de sintaxis abstracta (AST) y lo compila en un código de bytes optimizado con el intérprete. También hay un preparador, dos compiladores y almacenamiento en caché que lo hacen rápido (V8 tiene excelentes blogs sobre esto). Solo nos proporciona una pila de llamadas, un montón y un bucle de eventos ( más o menos, no realmente) . Podemos ignorar el montón a los efectos de este agujero de conejo: es un fragmento de memoria no estructurada con referencias a objetos y variables.

Bucle de eventos en tiempo de ejecución

A diferencia de Java o C++, JS permite la ejecución de un evento a la vez. Podemos ver esto en nuestros navegadores rastreando una ruta de llamada con console.trace():

// shoutout to ES6 arrow functions

foo = () => {
    console.log("Hello, World!");
    bar();
}
bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1049:6
Goodbye, World! VM1049:11 

// console.trace
trace @ VM1049:11
bar @ VM1049:7
foo @ VM1049:3

      
                
call stack visualization

Establecer tiempo de espera

Ejecutemos los mismos métodos con un retraso asíncrono de 5000 ms con setTimeout().

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1526:2

// 3 days later... 
Goodbye, World!  VM1526:6

// console.trace 
trace @ VM1382:10
bar @ VM1382:6

// setTimeout is invoked
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516

// foo is pushed to the stack first
foo @ VM1382:3

setTimeoutno es parte del motor JS, lo cual tiene sentido ya que el motor puede manejar un evento a la vez. Los navegadores , sin embargo, son de subprocesos múltiples y nos brindan API web para acceder a las implementaciones de setTimeout. Por ejemplo, Chrome usa el motor V8 de un solo subproceso en una arquitectura multiproceso, donde cada proceso tiene muchos subprocesos múltiples. En este caso, esto se llamó explícitamente con Cga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually, esto nos permite usar múltiples subprocesos.

El bucle de eventos

Reordenemos nuestro bloque de código:

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    setTimeout(() => console.log("Goodbye, World!"), 5000);
    trace();
}

trace = () => console.trace();

foo();

// output of foo 
Hello, World! VM1649:11 

// console.trace 
trace @ VM1694:11
bar @ VM1694:8
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516
foo @ VM1694:3
// end trace 

// 3 days later...
VM1649:6 Goodbye, World!

El JRE incluye:

  • JS Engine (un solo subproceso)
  • Java Virtual Machine (JVM): la máquina virtual que ejecuta el código en tiempo de ejecución. Por cierto, esta es la razón por la que "VM" antepone los registros de seguimiento.
  • API web
  • cola de tareas
  • Bucle de eventos

La pila de llamadas de nuestro motor muestra los eventos uno por uno. Una vez que ve setTimeout(), el método, junto con la devolución de llamada (referencia de función), se delega a la API web.

Tenga en cuenta que al motor ya no le importa cómo la implementación setTimeout de la API maneja el evento y la devolución de llamada. El motor pasa a mostrar el siguiente evento y enviarlo a Pacman (la JVM que ejecuta el código).

Mientras tanto, la API usa sus subprocesos para esperar 5000 ms y, posteriormente, los transfiere a la cola de tareas. El ciclo de eventos ve que el evento se alinea y lo vuelve a colocar en la pila de llamadas si y solo si la pila de llamadas está vacía.

Puede usar el divertido visualizador de Philip Roberts para verlo todo en acción. Además, echa un vistazo a su impresionante charla en JSConf en 2014.