Fermer

décembre 28, 2022

Comment le code JavaScript s’exécute : synchrone ou asynchrone


Dans cet article, nous passerons en revue un aperçu de haut niveau de la façon dont le code JavaScript synchrone et asynchrone est exécuté par le moteur JavaScript.

Un moteur JavaScript est un programme qui exécute du code JavaScript et le convertit dans un langage compris par l’ordinateur. Chaque navigateur Web contient un moteur JavaScript. Par exemple, V8 est le moteur JavaScript de Google Chrome et Node.js. Différents navigateurs ont différents moteurs JavaScript, mais ils implémentent le même concept de base sous le capot.

Avant d’entrer dans les détails, passons en revue certains des termes utilisés dans cet article :

  • Contexte d’exécution: Un contexte d’exécution est un environnement dans lequel le code JavaScript s’exécute et s’exécute. Un nouveau contexte d’exécution est créé chaque fois qu’une fonction est appelée ou invoquée. Nous avons deux types de contextes d’exécution : contexte d’exécution global et contexte d’exécution de fonction. Et un contexte d’exécution comporte deux phases : la création de la mémoire et l’exécution du code.

  • Environnement variable: Un environnement variable est l’endroit où le moteur JavaScript alloue de la mémoire dans des paires clé-valeur aux variables et fonctions dans un contexte d’exécution.

  • Pile d’appels: La pile d’appels est une partie du moteur JavaScript qui permet de garder une trace des appels de fonction. Lorsqu’une fonction est invoquée, elle est poussée vers la pile d’appels où son exécution commence, et lorsque l’exécution est terminée, la fonction est retirée de la pile d’appels. Il utilise le concept de piles dans les structures de données qui suit le principe Last-In-First-Out (LIFO).

  • Boucle d’événement : La boucle d’événements s’exécute indéfiniment et connecte la pile d’appels, la file d’attente de microtâches et la file d’attente de rappel. La boucle d’événements déplace les tâches asynchrones de la file d’attente de microtâches et de la file d’attente de rappel vers la pile d’appels chaque fois que la pile d’appels est vide.

  • File d’attente de rappel : Les fonctions de rappel pour setTimeout() sont ajoutées à la file d’attente de rappel avant d’être déplacées vers la pile des appels pour exécution.

  • File d’attente de microtâches : Les fonctions de rappel asynchrones pour les promesses et les observateurs de mutation sont mises en file d’attente dans la file d’attente des microtâches avant d’être déplacées vers la pile des appels pour exécution.

JavaScript synchrone

JavaScript est synchrone, bloquant et monothread. Cela signifie que le moteur JavaScript exécute notre programme de manière séquentielle, une ligne à la fois de haut en bas dans l’ordre exact des instructions.

Disons que nous avons trois console.log déclarations.

console.log("One")
console.log("Two")
console.log("Three")

Ce sera la sortie :

One 
Two
Three

Le moteur JavaScript ne peut pas exécuter la seconde console.log avant la première, et la troisième ne peut pas être exécutée avant la seconde. C’est ce que je veux dire quand je dis que JavaScript est synchrone et qu’il traite notre script ligne par ligne. Tant qu’une tâche en cours n’est pas terminée, la tâche suivante ne peut pas commencer.

Prenons un autre exemple :

function sayName(name){
  return name;
}
function greeting(){
  var myName = sayName('Ifeoma')
  console.log(`Hello ${name}`)
}
greeting()

Lorsque ce code s’exécute, voici ce qui se passe :

  1. Un tout nouveau contexte d’exécution appelé contexte d’exécution global sera créé et poussé vers la pile des appels. Il s’agit du contexte d’exécution principal, où notre code de niveau supérieur sera exécuté. Chaque programme a un seul contexte d’exécution global qui se trouve toujours au bas de la pile des appels.

  2. La phase de création de mémoire pour le contexte d’exécution global commence. Pendant la phase de création de la mémoire, les variables et les fonctions déclarées dans ce programme obtiennent de l’espace alloué en mémoire (alias variable environnement). Nous n’avons pas de variables déclarées dans la portée globale, donc les fonctions de cette portée se verront attribuer un espace en mémoire.

  3. Ensuite, la fonction SayName se voit attribuer un espace dans l’environnement variable et sa valeur est définie sur l’ensemble du corps de la fonction. Le code à l’intérieur de la fonction ne sera pas évalué car la fonction sayName n’a pas été invoqué.

  4. Ensuite, la fonction greeting se voit attribuer un espace dans l’environnement variable et sa valeur est également définie sur l’ensemble du corps de la fonction.

  5. La fonction greeting est invoqué sur la ligne suivante. Puisqu’il ne reste plus rien à ajouter à l’environnement variable, la phase d’exécution du code pour le contexte d’exécution global commence. Un tout nouveau contexte d’exécution pour la fonction greeting est créé et poussé vers le haut de la pile des appels. Maintenant, rappelez-vous que j’ai dit que chaque contexte d’exécution a deux phases. Pour ce contexte d’exécution, la phase d’allocation de mémoire commence.

  6. Sur la première ligne à l’intérieur du corps de la fonction, nous avons une variable appelée myName. Il se verra attribuer un espace en mémoire et initialisé avec la valeur undefined. (Noter: Pendant la phase de création de la mémoire, les variables ne reçoivent pas leurs valeurs ; les affectations se produisent dans la phase d’exécution du code. Lors de la phase de création de la mémoire, les variables déclarées avec let et const être initialisé avec uninitializedet les variables déclarées avec le var le mot-clé est initialisé avec undefined.

  7. Sur la ligne suivante, nous avons console.log(`Hello ${name}`), et c’est la fin de la phase de création de la mémoire pour cette fonction, donc la phase d’exécution du code commence. La variable myName reçoit le résultat d’un appel de fonction, donc la fonction sayName est invoqué et poussé vers la pile des appels.

  8. La fonction sayName accepte name comme paramètre, donc la variable name se voit attribuer un espace en mémoire et sa valeur est définie sur undefined. Sur la ligne suivante, nous avons un return instruction indiquant la fin de la fonction. La variable name se verra attribuer la valeur Ifeomala valeur est renvoyée par la fonction, et sayName est poussé hors de la pile des appels.

    Le thread d’exécution est maintenant de retour dans le contexte d’exécution pour le greeting une fonction. Il attribue la valeur de la name variable renvoyée par sayName à la variable myName. Nous avons un console.log déclaration sur la ligne suivante. Un contexte d’exécution est créé et poussé vers la pile d’appels ; ça imprime Hello Ifeoma à la console. C’est la fin de la greeting fonction, donc elle est retirée de la pile des appels.

  9. Nous revenons maintenant au contexte d’exécution global. Il n’y a plus rien à exécuter, donc il est également retiré de la pile des appels, et c’est la fin de notre programme.

Comme indiqué dans les étapes ci-dessus, JavaScript exige que chaque étape soit terminée avant que la suivante puisse commencer. Cela indique que jusqu’à ce qu’une tâche en cours soit terminée, la tâche suivante sera bloquée. Imaginez que vous avez une tâche qui prend du temps à accomplir ; rien d’autre ne peut se produire tant que cette tâche n’est pas terminée, ce qui peut faire apparaître le navigateur comme figé. Voyons comment nous pouvons créer des opérations asynchrones et comment le moteur JavaScript les gère.

JavaScript asynchrone

Contrairement aux opérations synchrones, une opération asynchrone n’empêche pas le démarrage de la tâche suivante même si la tâche en cours n’est pas encore terminée. Le moteur JavaScript fonctionne avec des fonctionnalités supplémentaires appelées API Web (setTimeout, setInterval, etc.) dans le navigateur Web, ce qui permet à JavaScript de se comporter de manière asynchrone.

Avec l’aide de ces API Web, JavaScript peut déplacer certaines tâches vers le navigateur pendant que JavaScript continue d’exécuter les opérations synchrones. En raison de ce comportement asynchrone, si nous avons une tâche qui peut prendre un certain temps (accès à une base de données, opérations sur le système de fichiers, etc.), la tâche asynchrone peut être transférée au navigateur pour qu’elle se produise en arrière-plan sans bloquer la prochaine tâche.

Dans l’exemple ci-dessous, j’utiliserai un setTimeout() fonction pour démontrer une opération asynchrone. Je n’inclurai pas de détails sur la façon dont la mémoire est allouée car je l’ai déjà expliqué ci-dessus.

console.log("first")

setTimeout(() => {
  console.log("second");
}, 3000)

console.log("third")

Lorsque ce code s’exécute, voici ce qui se passe :

  1. Un contexte d’exécution global sera créé et ajouté à la pile d’appels.

  2. Sur la première ligne, nous avons console.log("first"). Un contexte d’exécution sera créé pour lui et poussé vers la pile d’appels, first sera imprimé sur la console, puis retiré de la pile des appels.

  3. Sur la ligne suivante, nous avons un setTimeout() fonction, qui est l’une des API Web du navigateur. Il prend deux paramètres : une fonction de rappel comme premier paramètre et le temps (spécifié en ms) que vous voulez attendre avant d’exécuter la fonction de rappel comme deuxième paramètre. Un nouveau contexte d’exécution est créé et poussé vers la pile. Parce que setTimeout est une API Web, l’API Web enregistrera la fonction de rappel transmise à setTimeout dans l’environnement de l’API et déclenchez le minuteur dans le navigateur pendant 3000 ms, puis setTimeout est retiré de la pile des appels.

  4. Sur la ligne suivante, nous avons console.log("third"). Un contexte d’exécution est créé et ajouté à la pile des appels, third est imprimé dans la console, puis la fonction est retirée de la pile des appels.

  5. Dans l’environnement de l’API Web, nous avons toujours la fonction de rappel transmise à setTimeouten attendant que la minuterie expire après 3000 millisecondes.

  6. Disons que la minuterie est terminée. La fonction de rappel ne peut pas être déplacée directement de l’environnement de l’API Web vers la pile d’appels pour son exécution. Il doit attendre son tour, il est donc d’abord déplacé vers la file d’attente de rappel pour attendre que toutes les opérations synchrones aient été exécutées et que la pile des appels soit vide. Si nous avions un millier d’opérations après le setTimeout fonction, ils seraient tous exécutés avant la fonction de rappel pour setTimeout est déplacé vers la pile des appels.

  7. La boucle d’événements est responsable du déplacement des tâches asynchrones de la file d’attente de rappel vers la pile des appels chaque fois que la pile des appels est vide. La pile d’appels est maintenant vide, donc la boucle d’événements déplace la fonction de rappel vers la pile d’appels pour son exécution, et un nouveau contexte d’exécution est créé pour elle.

  8. Nous avons console.log("second") à l’intérieur de la fonction de rappel. L’instruction est ajoutée à la pile des appels, et second est imprimé sur la console, puis il est retiré de la pile des appels. Maintenant, en haut de la pile des appels, nous avons la fonction de rappel, et son exécution est terminée, elle est donc retirée de la pile des appels. Nous revenons au contexte d’exécution global, et puisqu’il ne reste plus rien à exécuter, il est également retiré de la pile des appels.

Outre la file d’attente de rappel, nous avons également la file d’attente de microtâches, qui a une priorité plus élevée. Les fonctions de rappel des promesses et des observateurs de mutation sont ajoutées à la file d’attente des microtâches. Lorsqu’une promesse est prête, le rappel de promesse est ajouté à la file d’attente des microtâches, où il doit attendre son exécution.

La boucle d’événements déplace à plusieurs reprises les fonctions de rappel de la file d’attente de microtâches et de la file d’attente de rappel vers la pile d’appels, mais la file d’attente de microtâches a une priorité plus élevée que la file d’attente de rappel. Les fonctions de rappel mises en file d’attente dans la file d’attente des microtâches seront toutes déplacées vers la pile des appels, une à la fois avant tout rappel de la file d’attente de rappel. Lorsque la file d’attente de microtâches est vide, la boucle d’événements peut commencer à déplacer les rappels de la file d’attente de rappel vers la pile des appels.

Conclusion

Dans cet article, nous avons vu un aperçu de haut niveau des étapes suivies par le moteur JavaScript pour exécuter du code synchrone et du code asynchrone. Nous avons également vu comment nous pouvons faire en sorte que JavaScript se comporte de manière asynchrone en utilisant les API Web fournies par le navigateur. Comprendre les bases du fonctionnement du moteur JavaScript sous le capot est fondamental pour les développeurs JavaScript qui souhaitent maîtriser le langage.




Source link

décembre 28, 2022