JavaScript sous le capot

Nov 28 2022
Table des matières Dans cet article, nous allons plonger dans le fonctionnement interne de JavaScript et comment il s'exécute réellement. En comprenant les détails, vous comprendrez le comportement de votre code et vous pourrez donc écrire de meilleures applications.

Table des matières

  • Thread et pile d'appels
  • Contexte d'exécution
  • Boucle d'événement et JavaScript asynchrone
  • Stockage de mémoire et récupération de place
  • Compilation JIT (juste à temps)
  • Sommaire

Dans cet article, nous allons nous plonger dans le fonctionnement interne de JavaScript et son fonctionnement réel. En comprenant les détails, vous comprendrez le comportement de votre code et vous pourrez donc écrire de meilleures applications.

JavaScript est décrit comme :

Langage de programmation monothread, ramasse-miettes, interprété ou compilé juste à temps avec une boucle d'événement non bloquante.

Découvrons chacun de ces termes clés.

Thread et pile d'appel :

Le moteur JavaScript est un interpréteur monothread composé d'un tas et d'une pile d'appels unique utilisée pour exécuter le programme.
La pile d'appels est une structure de données qui utilise le principe Last In, First Out (LIFO) pour stocker temporairement et gérer l'invocation de fonction (appel).
Cela signifie que la dernière fonction qui est poussée dans la pile est la première à sortir lorsque la fonction revient.
Étant donné que la pile d'appels est unique, l'exécution des fonctions se fait une par une, de haut en bas. Cela signifie que la pile d'appels est synchrone.

Maintenant, puisqu'il est synchrone, vous vous demanderez comment JavaScript peut gérer les appels asynchrones ?
Eh bien, la boucle d'événements est le secret de la programmation asynchrone de JavaScript.
Mais avant d'expliquer le concept d'appels asynchrones dans JavaScript et comment cela est possible avec un langage monothread, comprenons d'abord comment le code est exécuté.

Contexte d'exécution (EC) :

Le contexte d'exécution est défini comme l'environnement dans lequel le code JavaScript est exécuté.
La création d'un contexte d'exécution se déroule en deux phases :

1. Phase de création de la mémoire :

  • Création de l'objet global (qui s'appelle l'objet window dans le navigateur et l'objet global dans NodeJS).
  • Créer l'objet "this" et le lier à l'objet global.
  • Configuration d'un tas de mémoire (un tas est une grande région de mémoire, généralement non structurée) pour stocker des références de variables et de fonctions.
  • Stockage des fonctions et des variables dans un contexte d'exécution global en implémentant Hoisting .

Maintenant que nous connaissons les étapes d'exécution du code, revenons à la

Boucle d'événement :

Tout d'abord, commençons par regarder ce schéma :

Boucle d'événement dans JS

Nous avons le moteur qui se compose de deux composants principaux :
* Memory Heap - c'est là que l'allocation de mémoire se produit.
* Call Stack - c'est là que se trouvent vos cadres de pile lorsque votre code s'exécute.

Nous avons les API Web qui sont des threads auxquels vous ne pouvez pas accéder, vous pouvez simplement les appeler. Ce sont les éléments du navigateur dans lesquels la concurrence entre en jeu, comme le DOM, AJAX, setTimeout, et bien plus encore.

Enfin, il y a la file d'attente Callback qui est une liste d'événements à traiter. Chaque événement a une fonction associée qui est appelée pour le gérer.

Alors, quelle est la tâche de la boucle d'événements ici ?
La boucle d'événements a une tâche simple : surveiller la pile d'appels et la file d'attente de rappel. Si la pile d'appels est vide, la boucle d'événements prendra le premier événement de la file d'attente et le poussera vers la pile d'appels, qui l'exécute effectivement.
Une telle itération est appelée un tick dans la boucle d'événements. Chaque événement n'est qu'un rappel de fonction.

Stockage de mémoire et récupération de place :

Afin de comprendre la nécessité du ramasse-miettes, nous devons d'abord comprendre le cycle de vie de la mémoire qui est à peu près le même pour n'importe quel langage de programmation, il comporte 3 étapes principales.
1. Allouez la mémoire.
2. Utilisez la mémoire allouée pour lire ou écrire ou les deux.
3. Libérez la mémoire allouée lorsqu'elle n'est plus nécessaire.

La majorité des problèmes de gestion de la mémoire surviennent lorsque nous essayons de libérer la mémoire allouée. La principale préoccupation qui se pose est la détermination des ressources mémoire inutilisées.
Dans le cas des langages de bas niveau où le développeur doit décider manuellement quand la mémoire n'est plus nécessaire, les langages de haut niveau tels que JavaScript utilisent une forme automatisée de gestion de la mémoire connue sous le nom de Garbage Collection (GC).
JavaScript utilise deux stratégies célèbres pour effectuer GC : la technique de comptage de références et l'algorithme Mark-and-sweep.
Voici une explication détaillée de MDN sur les deux algorithmes et leur fonctionnement.

Compilation JIT (juste à temps):

Revenons à la définition de JavaScript : elle indique "Langage de programmation interprété et compilé par JIT", alors qu'est-ce que cela signifie ? Que diriez-vous de commencer par la différence entre un compilateur et un interpréteur en général ?

Par analogie, pensez à deux personnes avec des langues différentes qui veulent communiquer. Compiler, c'est comme s'arrêter et prendre tout son temps pour apprendre la langue, et interpréter, c'est comme avoir quelqu'un pour interpréter chaque phrase.

Ainsi, les langages compilés ont un temps d'écriture lent et un temps d'exécution rapide et les langages interprétés ont le contraire.

Parlons en termes techniques : la compilation est un processus de conversion du code source du programme en code binaire lisible par machine, avant l'exécution, et un compilateur prend l'intégralité du programme en une seule fois.

D'autre part, un interpréteur est un programme qui exécute les instructions du programme sans nécessiter qu'elles soient précompilées dans un format lisible par machine, et il prend une seule ligne de code à la fois.

Et voici le rôle de compilation JIT qui améliore les performances des programmes interprétés. L'ensemble du code est converti en code machine en une seule fois puis exécuté immédiatement .

À l'intérieur du compilateur JIT, nous avons un nouveau composant appelé un moniteur (alias un profileur). Ce moniteur surveille le code pendant son exécution et

  • Identifier les composants chauds ou tièdes du code ex : code répétitif.
  • Transformez ces composants en code machine pendant l'exécution.
  • Optimisez le code machine généré.
  • Échangez à chaud l'implémentation précédente du code.

Maintenant que nous avons compris les concepts de base, prenons une minute pour tout rassembler et résumer les étapes suivies par JS Engine lors de l'exécution du code :

Source de l'image : chaîne multimédia Traversy
  1. Le moteur JS prend le code JS écrit dans une syntaxe lisible par l'homme et le transforme en code machine.
  2. Le moteur utilise un parseur pour parcourir le code ligne par ligne et vérifier si la syntaxe est correcte. S'il y a des erreurs, le code cessera de s'exécuter et une erreur sera renvoyée.
  3. Si toutes les vérifications réussissent, l'analyseur crée une structure de données arborescente appelée arbre de syntaxe abstraite (AST).
  4. L'AST est une structure de données qui représente le code dans une structure arborescente. Il est plus facile de transformer le code en code machine à partir d'un AST.
  5. L'interpréteur prend ensuite l'AST et le transforme en IR, qui est une abstraction du code machine et un intermédiaire entre le code JS et le code machine. IR permet également d'effectuer des optimisations et est plus mobile.
  6. Le compilateur JIT prend ensuite l'IR généré et le transforme en code machine, en compilant le code, en obtenant des commentaires à la volée et en utilisant ces commentaires pour améliorer le processus de compilation.

Merci pour la lecture :)

Vous pouvez me suivre sur Twitter et LinkedIn .