Bien que la création de code de production à grande échelle puisse exiger une maîtrise approfondie de langages comme C++ et C, JavaScript permet souvent une programmation efficace avec une simple compréhension de ses fonctionnalités.
Des concepts comme le passage de fonctions de rappel ou l’écriture de code asynchrone ne sont généralement pas complexes à mettre en œuvre. Par conséquent, de nombreux développeurs JavaScript ne s’intéressent pas aux mécanismes sous-jacents. Ils ne se soucient pas de comprendre les complexités que le langage a volontairement rendues invisibles.
Toutefois, en tant que développeur JavaScript, il est de plus en plus crucial de saisir ce qui se passe réellement en coulisses et comment ces abstractions fonctionnent en réalité. Cette compréhension nous aide à prendre des décisions plus éclairées, ce qui peut, en retour, améliorer considérablement la performance de notre code.
Cet article explore un concept essentiel, mais souvent mal compris en JavaScript : la BOUCLE D’ÉVÉNEMENTS !
L’écriture de code asynchrone est inévitable en JavaScript, mais que signifie réellement l’exécution asynchrone ? C’est là qu’intervient la boucle d’événements.
Avant d’analyser le fonctionnement de la boucle d’événements, nous devons d’abord comprendre la nature de JavaScript et son mode d’exécution !
Qu’est-ce que JavaScript ?
Avant d’aller plus loin, revenons aux bases. Qu’est-ce que JavaScript, au juste ? Nous pouvons définir JavaScript comme :
JavaScript est un langage de haut niveau, interprété, mono-thread, non bloquant, asynchrone et concurrent.
Cette définition ressemble à une description de manuel, n’est-ce pas ? 🤔
Décortiquons-la !
Les mots-clés importants pour cet article sont : mono-thread, non bloquant, concurrent et asynchrone.
Mono-thread
Un thread d’exécution est la plus petite séquence d’instructions pouvant être gérée indépendamment par un planificateur. Un langage de programmation mono-thread ne peut exécuter qu’une seule tâche ou opération à la fois. Cela signifie qu’il exécutera un processus du début à la fin sans interruption du thread.
Contrairement aux langages multi-threads où plusieurs processus peuvent s’exécuter simultanément sur différents threads sans se bloquer mutuellement.
Comment JavaScript peut-il être mono-thread et non bloquant en même temps ?
Mais, au fait, que signifie bloquer ?
Non bloquant
Il n’existe pas de définition unique du blocage ; cela fait simplement référence aux opérations qui s’exécutent lentement sur le thread. Par conséquent, non bloquant signifie que les opérations ne sont pas lentes sur le thread.
Mais n’ai-je pas dit que JavaScript s’exécute sur un seul thread ? Et je l’ai également décrit comme non bloquant, ce qui signifie que les tâches s’exécutent rapidement sur la pile d’appels ? Comment cela est-il possible ? Qu’en est-il des temporisations et des boucles ?
Pas de panique ! Nous allons le découvrir bientôt 😉.
Concurrent
La concurrence implique l’exécution simultanée de code par plusieurs threads.
D’accord, ça devient bizarre. Comment JavaScript peut-il être mono-thread tout en étant concurrent, c’est-à-dire exécuter du code sur plus d’un thread ?
Asynchrone
La programmation asynchrone signifie que le code s’exécute dans une boucle d’événements. Lorsqu’une opération bloquante se produit, un événement est déclenché. Le code bloquant continue son exécution sans bloquer le thread d’exécution principal. Une fois l’opération bloquante terminée, son résultat est mis en file d’attente et renvoyé dans la pile.
Mais JavaScript possède un seul thread ! Qu’est-ce qui exécute ce code bloquant tout en permettant l’exécution d’autres portions de code ?
Avant de continuer, récapitulons ce que nous avons vu jusqu’à présent.
- JavaScript est mono-thread
- JavaScript est non bloquant, c’est-à-dire que les processus lents ne bloquent pas son exécution
- JavaScript est concurrent, c’est-à-dire qu’il exécute du code sur plusieurs threads en même temps
- JavaScript est asynchrone, c’est-à-dire qu’il exécute le code bloquant ailleurs.
Cependant, ces points ne concordent pas parfaitement. Comment un langage mono-thread peut-il être non bloquant, concurrent et asynchrone ?
Allons plus loin et examinons les moteurs d’exécution JavaScript, tels que V8. Peut-être y a-t-il des threads cachés dont nous ne sommes pas conscients.
Moteur V8
Le moteur V8 est un moteur d’exécution WebAssembly open source haute performance pour JavaScript, écrit en C++ par Google. La plupart des navigateurs utilisent le moteur V8 pour exécuter JavaScript, et même l’environnement d’exécution populaire Node.js l’utilise également.
En termes simples, V8 est un programme C++ qui prend du code JavaScript, le compile et l’exécute.
V8 effectue deux opérations principales :
- Allocation de mémoire de tas
- Contexte d’exécution de la pile d’appels
Malheureusement, nos soupçons étaient infondés. V8 ne possède qu’une seule pile d’appels. Considérons la pile d’appels comme le thread.
Un thread === une pile d’appels === une exécution à la fois.
Image – Hacker Midi
Étant donné que V8 n’a qu’une seule pile d’appels, comment JavaScript s’exécute-t-il simultanément et de manière asynchrone sans bloquer le thread d’exécution principal ?
Essayons de le découvrir en écrivant un code asynchrone simple et courant, puis analysons-le ensemble.
JavaScript exécute chaque ligne de code séquentiellement (mono-thread). Comme prévu, la première ligne s’affiche dans la console, mais pourquoi la dernière ligne s’affiche-t-elle avant le code de temporisation ? Pourquoi le processus d’exécution n’attend-il pas que le code de temporisation (bloquant) soit terminé avant d’exécuter la dernière ligne ?
Un autre thread semble avoir permis l’exécution de cette temporisation, car nous savons qu’un thread ne peut exécuter qu’une seule tâche à la fois.
Jetons un coup d’œil au code source V8 pendant un moment.
Quoi ?! Il n’y a pas de fonctions de temporisation dans V8, pas de DOM ? Pas d’événements ? Pas d’AJAX ?…. Effectivement !!!
Les événements, le DOM, les temporisations, etc. ne font pas partie de l’implémentation principale de JavaScript. JavaScript est strictement conforme aux spécifications ECMAScript, et différentes versions sont souvent référencées en fonction de leurs spécifications ECMAScript (ES X).
Flux de travail d’exécution
Les événements, les temporisations, les requêtes AJAX sont tous fournis côté client par les navigateurs et sont généralement appelés API Web. Ce sont eux qui permettent au JavaScript mono-thread d’être non bloquant, concurrent et asynchrone ! Mais comment ?
Le flux de travail d’exécution d’un programme JavaScript comprend trois sections principales : la pile d’appels, l’API Web et la file d’attente des tâches.
La pile d’appels
Une pile est une structure de données où le dernier élément ajouté est toujours le premier à être retiré. On peut la comparer à une pile d’assiettes : seule la première assiette (la dernière ajoutée) peut être retirée. La pile d’appels est simplement une structure de données de pile où les tâches ou le code sont exécutés dans l’ordre.
Prenons l’exemple ci-dessous :
Source – https://youtu.be/8aGhZQkoFbQ
Lorsque vous appelez la fonction printSquare()
, elle est empilée. La fonction printSquare()
appelle la fonction square()
. La fonction square()
est empilée et appelle la fonction multiply()
. La fonction multiply()
est également empilée. Étant donné que la fonction multiply()
est retournée et est la dernière chose à avoir été empilée, elle est résolue en premier et est retirée de la pile, suivie de square()
puis de printSquare()
.
L’API Web
C’est là que le code qui n’est pas géré par le moteur V8 est exécuté, afin de ne pas « bloquer » le thread d’exécution principal. Lorsque la pile d’appels rencontre une fonction d’API Web, le processus est immédiatement transféré vers l’API Web où il est exécuté et libère la pile d’appels pour effectuer d’autres opérations pendant son exécution.
Reprenons notre exemple setTimeout
ci-dessus :
Lorsque nous exécutons le code, la première ligne console.log
est empilée, et nous obtenons notre sortie presque immédiatement. En atteignant la temporisation, les temporisations sont gérées par le navigateur et ne font pas partie de l’implémentation principale de V8. Elles sont donc transférées à l’API Web, ce qui libère la pile pour d’autres opérations.
Pendant que la temporisation est toujours en cours d’exécution, la pile passe à la ligne d’action suivante et exécute le dernier console.log
, ce qui explique pourquoi nous obtenons cette sortie avant celle de la temporisation. Une fois la temporisation terminée, quelque chose se produit. Le console.log
de la temporisation réapparaît comme par magie dans la pile d’appels !
Comment ?
La boucle d’événements
Avant de parler de la boucle d’événements, examinons le rôle de la file d’attente des tâches.
Revenons à notre exemple de temporisation. Une fois que l’API Web a terminé d’exécuter la tâche, elle ne la renvoie pas automatiquement dans la pile d’appels. Elle la place dans la file d’attente des tâches.
Une file d’attente est une structure de données qui suit le principe « premier entré, premier sorti ». Les tâches sont traitées dans l’ordre dans lequel elles ont été ajoutées. Les tâches exécutées par l’API Web, qui sont ajoutées à la file d’attente des tâches, sont ensuite renvoyées à la pile d’appels pour que leur résultat s’affiche.
Mais, au fait, QU’EST-CE QUE LA BOUCLE D’ÉVÉNEMENTS ???
Source – https://youtu.be/8aGhZQkoFbQ
La boucle d’événements est un processus qui attend que la pile d’appels soit vide avant d’empiler des rappels de la file d’attente des tâches. Une fois la pile vide, la boucle d’événements se déclenche et vérifie la file d’attente des tâches pour voir si des rappels sont disponibles. Si c’est le cas, elle les empile, attend que la pile d’appels soit de nouveau vide, puis répète le même processus.
Source – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell
Le diagramme ci-dessus illustre le flux de travail de base entre la boucle d’événements et la file d’attente des tâches.
Conclusion
Bien qu’il s’agisse d’une introduction très basique, le concept de la programmation asynchrone en JavaScript donne suffisamment d’informations pour comprendre clairement les mécanismes sous-jacents et comment JavaScript peut s’exécuter simultanément et de manière asynchrone avec un seul thread.
JavaScript reste très demandé. Si vous êtes curieux d’en apprendre davantage, je vous conseille de consulter ce cours Udemy.