Async JS sous le capot

Jan 27 2023
MDN définit le workflow asynchrone comme ..

MDN définit le workflow asynchrone comme

..démarrer une tâche potentiellement longue et être toujours capable de répondre à d'autres événements pendant que cette tâche s'exécute… Une fois cette tâche terminée, votre programme est présenté avec le résultat.

Mais comment cela est-il réalisable sur le Web avec JavaScript ?

Avant de plonger dans les implémentations asynchrones, il est utile d'avoir une compréhension de base de JS à un niveau inférieur.

Comment fonctionne JS ?

JavaScript est mono-thread et interprété, ce qui signifie que les instructions sont exécutées directement et n'ont pas d'étape de compilation ex-ante. Prenons l'exemple du V8, le moteur le plus populaire.

À un niveau élevé, V8 analyse le code, crée un arbre de syntaxe abstraite (AST) et le compile en bytecode optimisé avec l'interpréteur. Il y a aussi un préparateur, deux compilateurs et une mise en cache qui le rendent rapide (V8 a d'excellents blogs à ce sujet). Il ne nous fournit qu'une pile d'appels, un tas et une boucle d'événements ( en quelque sorte, pas vraiment) . Nous pouvons ignorer le tas pour les besoins de ce trou de lapin : c'est un morceau de mémoire non structurée avec des références d'objet et des variables.

Boucle d'événements d'exécution

Contrairement à Java ou C++, JS permet l'exécution d'un événement à la fois. Nous pouvons le voir dans nos navigateurs en traçant un chemin d'appel avec 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

SetTimeout

Exécutons les mêmes méthodes avec un délai asynchrone de 5000 ms avec 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

setTimeoutne fait pas partie du moteur JS, ce qui est logique puisque le moteur peut gérer un événement à la fois. Les navigateurs , cependant, sont multithreads et nous fournissent des API Web pour accéder aux implémentations de setTimeout. Par exemple, Chrome utilise le moteur V8 à thread unique dans une architecture multi-processus, où chaque processus est fortement multi-thread. Dans ce cas, cela a été explicitement appelé avec Cga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually, cela nous permet d'utiliser plusieurs threads.

La boucle d'événements

Réorganisons notre bloc de code :

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!

Le JRE comprend :

  • Moteur JS (single-thread)
  • Java Virtual Machine (JVM) — La machine virtuelle qui exécute le code dans le runtime. Au fait, c'est pourquoi "VM" ajoute les journaux de suivi.
  • API Web
  • File d'attente des tâches
  • Boucle d'événement

La pile d'appels de notre moteur affiche les événements un par un. Une fois qu'il voit setTimeout(), la méthode, ainsi que le rappel (référence de la fonction), est déléguée à l'API Web.

Notez que le moteur ne se soucie plus de la manière dont l'implémentation setTimeout de l'API gère l'événement et le rappel. Le moteur passe à l'événement suivant et le transmet à Pacman (la JVM qui exécute le code).

Pendant ce temps, l'API utilise ses threads pour attendre 5000 ms et les transmet ensuite à la file d'attente des tâches. La boucle d'événements voit que l'événement se met en ligne et le repousse sur la pile des appels si et seulement si la pile des appels est vide.

Vous pouvez utiliser le visualiseur amusant de Philip Roberts pour tout voir en action. Découvrez également son discours génial à JSConf en 2014.