Dans cet article, vous apprendrez comment exécuter JavaScript dans le navigateur. Le navigateur utilise le moteur d'exécution JavaScript (comme V8 dans le cas de Google Chrome), CallStack . Boucle d'événement pour l'exécution de JavaScript. JavaScript est conçu pour être utilisé en tant que langage mono-thread, mais nous pouvons exécuter du code JavaScript asynchrone dans le navigateur sans bloquer le thread principal. Cet article vise à fournir une explication détaillée des outils sous-jacents utilisés par le navigateur pour exécuter du code JavaScript. Vous apprendrez également comment le code asynchrone est exécuté à l'aide de la boucle d'événement. Si vous suivez ce tutoriel, certains des concepts de base de JavaScript, y compris sa nature asynchrone, ses fermetures, son levage et sa chaîne de champs d'application, prendront tout leur sens!
Cet article est la troisième partie de la série sur The Journey de JavaScript: du téléchargement de scripts à l'exécution . Avant de plonger dans l’exécution de JavaScript, rappelons tout ce que nous avons appris dans les précédents deux articles de la série:
- Les balises du script
- Dans la partie II de la série, nous avons approfondi notre compréhension des composants internes des moteurs JavaScript. Les moteurs utilisent la compilation Just-In-Time pour produire le code intermédiaire. Le compilateur qui optimise optimise ensuite le bytecode.
Je recommande vivement de lire les deux parties précédentes de la série avant de passer à la section exécution, car elles reposent sur les principes de base des moteurs JavaScript.
Table of Contents [19659008] Contexte d'exécution
Passons en revue certains concepts essentiels nécessaires à la compréhension de l'exécution de JavaScript:
Contexte d'exécution
Le contexte d'exécution peut être considéré comme un conteneur. qui contient des variables d fonctions. Considérons un exemple simple pour mieux comprendre:
var a = 'Bonjour le monde!'
fonction helloWorld () {[19659027] var a = 'Bonjour, fonction!'
console . log ( "Valeur de l'intérieur de la fonction helloWorld" a )
}
helloWorld ([19659025])
console . log ( "Valeur d'un extérieur de la fonction helloWorld" a )
Il s'agit d'un code simple et direct en JavaScript, mais il est très important de comprendre comment le code ci-dessus sera exécuté par le moteur JavaScript. Lorsque le code ci-dessus est prêt à être exécuté, le thread principal crée d'abord un gros conteneur pour l'ensemble du programme. Ce conteneur s'appelle Contexte global d'exécution . Il stocke toutes les variables et fonctions nécessaires pendant toute la durée du programme. Le diagramme ci-dessous reflète l'état du contexte d'exécution global:
En JavaScript, chaque fonction fonctionne dans son propre contexte d'exécution. Essayons d’adapter cette affirmation à l’analogie du conteneur: chaque fonction JavaScript, ainsi que ses variables locales, s’insère dans un autre petit conteneur situé dans le conteneur principal. Le plus petit conteneur peut accéder aux variables du grand conteneur; toutefois, les conteneurs les plus grands ne peuvent pas accéder aux variables des conteneurs les plus petits de l'intérieur.
Visualisons le contexte d'exécution de la fonction helloWorld
:
Veuillez noter que les conteneurs les plus petits à l'intérieur donnent la préférence. à leur contenu local. Ils vérifient d'abord si la variable en question est présente dans la plage spécifiée et, si elle n'est pas présente, ils la demandent à son conteneur parent.
Si vous avez compris l'analogie de conteneur pour le contexte d'exécution, la sortie du console
les déclarations dans le code ci-dessus devraient vous paraître évidentes! La valeur de a
dans la fonction helloWorld
est Hello Function!
. En dehors de cela, la valeur est Hello World
car la fonction helloWorld
a son propre contexte d'exécution qui contient la variable un
avec la valeur Hello Function!
. La variable a
dans la fonction helloWorld
n'est pas accessible en dehors de celle-ci et, par conséquent, sa valeur est Hello World!
dans le contexte de l'exécution globale.
Terminologies Lié au contexte d'exécution
Le bloc principal dans lequel se trouve l'ensemble du programme s'appelle le contexte mondial d'exécution . Chaque fonction est exécutée dans son propre contexte et est appelée contexte de fonction ou contexte d’exécution locale . La récente avancée de JavaScript avec ECMAScript version 6 (ES6) permet de conserver le contexte d’exécution lexicale ou le contexte d’exécution au niveau du bloc avec l’utilisation de de
et de const
.
Comprenons le contexte d'exécution lexicale à l'aide d'un exemple:
pour ( let i = 0 ; i < 5 ; i ++ ) {
console . log ( 'valeur de i dans la boucle for' i )
}
console . log ( 'valeur de i en dehors de la boucle for' i )
La variable i
est non défini en dehors de la boucle pour
en raison de l'utilisation de laissez
crée un contexte d'exécution au niveau du bloc et sa durée de vie correspond à la parenthèse fermante }
. Le contexte d'exécution créé par i
est appelé le contexte d'exécution lexical
Scopes and Scope Chain
Nous avons appris à analyser dans la première partie de la série . Le travail de l'analyseur consiste à créer des portées et l'arbre de syntaxe abstraite (AST). Nous avons déjà beaucoup appris sur AST et voyons maintenant quelles sont les portées et comment elles ont été créées.
Les portées définissent la zone d’opération d’une variable particulière . Une variable définie dans le contexte global exécuté est étendue au niveau global et est accessible à n’importe quelle fonction du programme. Les variables définies dans un contexte d’exécution local sont définies au niveau local. Nous avons donc une portée globale, locale et lexicale.
Passons maintenant aux chaînes de portée:
Les fonctions locales ont accès à leur portée locale ainsi qu’à leur portée globale. S'il existe une fonction imbriquée dans cette fonction locale, elle aura accès à sa propre portée, à celle de son parent et à la portée globale. Cela forme une chaîne d'étendues et s'appelle donc une chaîne d'étendues. Voyons cela en action!
var a = 1
fonction foo () {
var b = a + 1
function bar () {
var c = ] b + a
}
bar ()
}
foo ()
Lorsque nous exécutons le code ci-dessus, l'interprète crée une fonction pour la fonction foo
et la chaîne de son étendue ressemblent à quelque chose comme [foo scope, global scope]
. Le champ d'action de la fonction bar
serait de [bar scope, foo scope, global scope]
.
La fonction bar
a accès aux variables de sa fonction extérieure bar
en raison de la chaîne d'étendue. . Pouvons-nous en déduire quelque chose d'intéressant? Permettez-moi de citer la définition de Fermetures
qui devrait maintenant prendre tout son sens:
Une fermeture est la combinaison d’une fonction et de l’environnement lexical dans lequel cette fonction a été déclarée. – MDN
L'utilisation du mot combinée
devrait être évidente, car elle combine les champs d'application des fonctions internes et externes pour former sa propre chaîne de champs d'application. J'espère que vous savez maintenant qu'une fermeture n'est pas magique, mais une combinaison d'étendues.
Look-ups sur la chaîne d'étendues
Lorsque l'interprète rencontre une variable lors de l'exécution d'un morceau de code, il commence par en rechercher la valeur. portée actuelle. Il parcourt la chaîne des étendues jusqu'à ce que la variable soit trouvée ou qu'il ait atteint l'extrémité de la chaîne.
Recherches sur la chaîne de prototypes
Voyons un exemple pour comprendre le fonctionnement des étendues avec les chaînes de prototypes: [19659020] var obj = {}
function baz () {
obj . a = 'Bonjour'
return function barr () {
console . log ( 'Property a set on obj' obj . a )
}
}
var bazReturnFn = baz ()
bazReturnFn ()
Le code ci-dessus est direct. La fonction baz
renvoie une fonction anonyme. La variable bazReturnFn
enregistre la valeur de retour de la fonction baz
. La fonction bazReturnFn
est exécutée dans un contexte global. Il trouve la variable obj
dans le contexte global, puis recherche la propriété a
sur obj
.
Définissons la propriété a
. ] sur le prototype de obj
:
var obj = {}
fonction baz () 19659024] {
obj . prototype . a = 'Bonjour'
renvoyer fonction bar () {
console . log ( 'Property a set on obj' obj . a )
}
}
var bazReturnFn = baz ()
bazReturnFn ()
Il retourne toujours la même valeur! Veuillez noter que l'interprète parcourt d'abord toute la chaîne de la portée, puis passe à la chaîne du prototype pour trouver la valeur d'une variable.
Maintenant que nous avons bien compris la terminologie, passons aux deux phases importantes impliquées dans l'exécution de JavaScript.
Phase de création
Lorsqu'une fonction est appelée, l'interpréteur crée son contexte d'exécution, puis la fonction entre en phase de création. L'interprète parcourt le code de fonction ligne par ligne et effectue les opérations suivantes:
- Crée la chaîne de champs
- Crée les variables, les fonctions et les arguments
- Détermine la valeur de
ce
pour l'élément en cours. context
Remarque: la fonction n'est pas encore exécutée.
Représentation d'un contexte d'exécution:
fnExecutionContext = {
scopeChain : {
}
variableObject : {
}
ceci : {}
}
Phase d'activation
La fonction est exécutée dans cette phase et les variables définies dans variableObjet
sont initialisées ici
Comprenons ceci à l'aide d'un exemple simple:
fonction foo ( a ] b ) {
var c = a + b
function bar () {
retour c
}
retour bar ()
}
foo ( 1 2 )
] Lorsque la fonction foo
est invoquée, l'interprète crée d'abord son contexte d'exécution comme suit:
fooExecutionContext = {
scopeChain : {}
variableObject : {}
cette : {}
La fonction foo
maintenant entre dans la phase de création. Dans cette phase, l'interpréteur crée la chaîne d'étendue, scanne le code sans l'exécuter et déclare toutes ses variables dans le variableObject
et évalue la valeur de de ce
. Voyons l’état du contexte d’exécution après cette phase:
fooExecutionContext = {
scopeChain : { }
variableObject : {
arguments : {
0 : 1
1 : 2
longueur : 2
}
a : 1
b : 2
c : indéfini
bar : 'pointeur sur la fonction bar ()'
}
ce : {}
Veuillez noter: les arguments passés à la fonction foo
sont évalués dans la phase de création. La variable locale c
est déclarée sur la variableObject
et est initialisée avec une valeur de non définie
.
L'évaluation de de cette
n'est pas dans le cadre de cet article. Je recommande de lire Voyons maintenant ce
une fois pour toutes pour bien comprendre en JavaScript.
Voyons maintenant l'état de l'exécution. contexte lorsque la fonction entre dans la phase d'activation:
fooExecutionContext = {
scopeChain : { }
variableObject : {
arguments : {
0 : 1
1 : 2
longueur : 2
}
a : 1
b : 2
c : 3
bar : 'pointeur sur la fonction bar ()'
}
ce : {}
Notez la valeur de la variable c
. L'interprète évalue la valeur de c
lorsqu'il rencontre cette ligne var c = a + b
. Lorsqu'elle rencontre l'invocation de la fonction bar
elle crée d'abord le contexte d'exécution de bar
puis la fonction bar
entre dans la phase de création où l'interprète crée. sa chaîne de domaine, variableObject
et évalue la valeur de ce
. Dans le cas de la fonction bar
la chaîne de son étendue aura son propre contexte, ainsi que le contexte de la fonction foo
et donc la valeur de c
n'est pas undefined
dans la fonction bar
.
Inférences tirées de notre compréhension des deux phases
Compréhension du tableau des arguments
La première fois que j'ai entendu parler du arguments
tableau, j’ai pensé qu’il s’agissait d’une structure de données magique qui apparaît seule.
function add () {
var a = arguments [ 0 ]
var b = arguments [ 1 ] [19659027] retour a + b
}
add ( 1 2 )
Où sont définis les arguments
et comment définit-il le tableau? function add
obtenir la référence? Je n'ai même pas déclaré le tableau d'arguments
mais mon code fonctionne parfaitement!
Après avoir appris l'exécution de JavaScript, j'ai compris que le tableau d'arguments
est pas de magie mais un tableau créé par l'interprète pendant la phase de création. Sigh!
Un mot sur le levage
Une définition stricte du levage indique que les déclarations de variable et de fonction sont physiquement déplacées en haut de votre code, mais ce n'est pas ce qui se produit réellement. Au lieu de cela, les déclarations de variable et de fonction sont mises en mémoire pendant la phase de compilation, mais restent exactement à l'endroit où vous les avez entrées dans votre code. - MDN
Hmm. Essayons de comprendre la déclaration ci-dessus:
L’interprète analyse le code de fonction complet et place les déclarations de variable et de fonction dans variableObject
du contexte d’exécution pendant la phase de création. Les déclarations ne sont déplacées nulle part. ils sont simplement ajoutés dans variableObject
et conservés de côté dans la mémoire.
Nous avons déjà appris beaucoup de choses, telles que le contexte d'exécution, les portées et les chaînes de portées, les fermetures et le levage.
Cool , mais qu’en est-il de CallStack, MemoryHeap, Callback Queue et Event Loop?
Le moteur JavaScript n’est pas le seul responsable de l’exécution de JavaScript dans le navigateur. CallStack, MemoryHeap, Callback Queue et Event Loop jouent ici un rôle important! Voyons-les un à un:
CallStack et MemoryHeap
CallStack est une structure de données Stack qui stocke les instructions JavaScript de manière séquentielle pour leur exécution. Cela aide l’interprète à savoir ce qui doit être exécuté ensuite!
Disons que nous avons une fonction foo
comme ci-dessous:
fonction add ( a b ) {
var c = a ] + b ;
return c ;
}
function foo () {
var. a = 1 ;
var b = 2 ;
var c = ] add ( a b )
console . log ( 'valeur d'un' a ) ;
console . log ( 'valeur de b' b ) ;
console . log ( 'valeur de c' c ) ;
}
foo ([19659025])
La pile d'appels pour le code ci-dessus se présente comme suit:
Notez que les instructions sont insérées dans la pile puis exécutées une par une par l'interprète.
MemoryHeap est utilisé pour stocker les variables et les fonctions.
Qu'est-ce qu'un code asynchrone et comment est-il exécuté?
Comme on peut le voir sur le diagramme ci-dessus sur callstack, les instructions sont exécutées instantanément et ne le sont pas. bloquer la pile. Cependant, certains morceaux de code prennent du temps à s'exécuter. Par exemple, les appels réseau prennent un certain temps pour répondre aux appels par fil
Cela signifie-t-il que le code JavaScript responsable d'un appel réseau restera sur la pile jusqu'à ce qu'il ait terminé son exécution et récupère quelque chose du serveur? Cela signifierait que la pile d'appels ne peut accepter aucun événement dans cette période. L'écran entier semblera ne pas répondre! Il est clair que nous devons penser à de meilleures alternatives pour traiter un code qui prend du temps à s'exécuter.
Les appels réseau, les minuteurs et les autres bits de code qui prennent du temps à s'exécuter sont placés dans un thread séparé sans bloquer le callstack principal. Ils s'exécutent de manière asynchrone dans un autre thread et, une fois leur exécution terminée, quelqu'un appelle la CallStack à la faire avancer!
Voici une illustration de la manière dont CallStack, MemoryHeap, Callback Quack et Event Loop fonctionnent ensemble pour exécuter JavaScript. code:
Voyons ceci avec un exemple:
function foo () {
var a = "Bonjour le monde"
setTimeout ( function bar () {
console . log ( "Barre de fonction interne" )
} 1000 )
}
La première déclaration dans le code ci-dessus ne prend presque pas de temps à exécuter. La deuxième instruction setTimeout
prend comme premier argument une fonction bar
et cette fonction doit être exécutée après
1000ms
comme indiqué dans son deuxième argument.
setTimeout
ne restera pas dans le CallStack pendant à 1000ms
et bloquera tout. Au lieu de cela, il serait poussé à un autre thread pour 1000ms
. Après 1000ms
la fonction bar
sera poussée dans la file d'attente des rappels (une autre structure de données qui gère les rappels pour les pièces asynchrones).
Event Loop
Quelqu'un lance un cri CallStack a appris que le code asynchrone est exécuté. Vous pouvez gérer le reste! Ce quelqu'un est Event Loop .
La boucle d'événement est située entre CallStack et la file d'attente de rappel. Chaque fois qu'il voit que CallStack est vide, il place simplement les éléments de la file d'attente de rappel dans CallStack pour exécution. A ce moment, la fonction barrée
serait exécutée par le CallStack
Veuillez noter que le CallStack peut devenir vide après 1000ms
et pas exactement à 1000ms
. . D'où les appels setTimeout
et setInterval
garantissent que les rappels seront exécutés à tout moment mais pas avant l'intervalle.
Et c'est ainsi que le code asynchrone est exécuté dans le navigateur! 19659402] Conclusion
Il s’agit de la dernière partie de la série trois sur Le périple de JavaScript: du téléchargement de scripts à l’exécution . Nous avons commencé par la façon dont les scripts sont téléchargés, puis analysés par le moteur JavaScript. Nous avons vu comment les analyseurs génèrent les portées et l'arbre de syntaxe abstraite. Nous avons appris tout ce qu'il faut savoir sur Ignition et Turbofan dans le moteur JavaScript V8, ainsi que sur la façon dont les tableaux et les modèles d'objet sont gérés par le moteur.
Dans cette partie, nous avons vu comment le code JavaScript est réellement exécuté par le navigateur. Nous avons appris tout sur le contexte et les portées d'exécution et sur la façon dont le moteur JavaScript prend en charge les fermetures et le levage. Nous avons également entendu parler des CallStacks, MemoryHeap, Callback Queue et Event Loop.
Ce message vous a été proposé par Kendo UI
Vous souhaitez en savoir plus sur la création d’applications Web géniales? Tout commence avec Kendo UI - la bibliothèque complète de composants d'interface utilisateur qui vous permet de créer rapidement des applications réactives de haute qualité. Il comprend tout ce dont vous avez besoin, des grilles et graphiques aux menus déroulants et jauges.
Source link