Cet article vous aidera à comprendre les éléments internes de JavaScript – même les parties les plus étranges. Chaque ligne de code que vous écrivez en JavaScript sera parfaitement logique une fois que vous saurez comment il a été interprété par le moteur sous-jacent. Vous apprendrez plusieurs manières de télécharger des scripts en fonction du cas d'utilisation et de la manière dont l'analyseur génère un arbre de syntaxe abstraite et ses méthodes heuristiques lors de l'analyse du code. Intéressons-nous de plus près aux composants internes des moteurs JavaScript – à commencer par le téléchargement de scripts.
Le langage JavaScript est l’un des langages les plus populaires. Fini l'époque où les gens utilisaient JavaScript uniquement pour gérer des écouteurs d'événements DOM et quelques tâches peu exigeantes. Aujourd'hui, vous pouvez créer une application complète à partir de la base en utilisant JavaScript. JavaScript a pris le dessus sur les vents, les terres et les mers. Avec Node.js envahissant la gamme des technologies côté serveur et l'avènement de bibliothèques riches et puissantes et de frameworks tels que React, Angular et Vue, JavaScript a conquis le Web. Les applications envoient beaucoup de JavaScript sur les câbles. Presque toutes les tâches compliquées d'une application sont maintenant implémentées à l'aide de JavaScript.
Bien que tout cela soit formidable, il est décourageant de constater que la plupart de ces applications sont dépourvues d'expérience utilisateur minimale. Nous continuons à ajouter des fonctionnalités à notre application sans prendre en compte ses implications en termes de performances. Il est important de suivre les techniques appropriées pour obtenir un code optimisé.
Dans cette série de didacticiels, nous allons tout d'abord comprendre ce qui ne va pas avec les techniques classiques, puis nous approfondirons l'analyse pour en apprendre davantage sur certaines Aidez-nous à écrire du code optimisé. Nous comprendrons également comment notre code est analysé, interprété et compilé par le moteur JavaScript sous-jacent et ce qui fonctionne le mieux pour nos moteurs. Bien que la syntaxe de JavaScript soit assez facile à comprendre, la compréhension de ses éléments internes est une tâche plus ardue. Nous allons commencer par les bases et finalement reprendre la bête.
Comprendre la balise de script
Considérons un fichier HTML simple:
< html >
< head >
< script src = ' ./ js / first.js ' > </ script > [19659011] < script src = ' ./ js / second.js ' > </ script [19659008]>
< script src = ' ./ js / third.js ' > </ script >
< script src = ' ./ js / third.js ' > > ] </ script >
</ head >
< body >
< div > [19659000] ] Description de la balise script </ div >
</ corps >
</ html >
first.js inclut le code suivant:
console . log ( "fichier first.js" )
second.js contient le code suivant :
console . log ( "fichier second.js" )
J'ai mis en place un serveur express pour démontrer les concepts expliqués dans l'article. Si vous souhaitez expérimenter en cours de route, n'hésitez pas à cloner mon référentiel GitHub .
Voyons ce qui se passe lorsque nous ouvrons ce fichier HTML dans le navigateur:
< html >
< head >
< script asynchrone src = ' ./ js / first.js '
> </ script >
< script async src = ' ./ js / seconde.js ' > </ script >
< script src = ' ./ js / third .js ' > </ script >
< script src = ' . /js/fourth.js'[19459004hootingde19659008]>[19659016unset</[19459004Scripts19269004_reverscript/19199099499179499179499179body>
< div > [19659045] Description de la balise de script </ de >
</ de corps >
</ html >
Lorsque vous ajoutez le mot clé defer dans la balise de script, le navigateur n’exécute ce script qu’après la fin de l’analyse HTML. Différer signifie simplement que l'exécution du fichier est différée ou différée. Le script est téléchargé dans un autre thread et est exécuté uniquement à la fin de l'analyse HTML. Report de l’exécution des scripts
< html >
< head >
< script defer src = ' ./ js / first.js ' > </ script >
< script différer src = ' ./ js / second.js ' > </ script >
< script src = ' ./ js / third.js '
> [19659016] </ script > < script src = ' ./ js / quatrième.js ' 19659008]> </ script > </ head > < corps > < div [19659008]> Description de la balise de script </ div [19659008]> </ corps > </ html >
{
"type" : "Programme"
"start" : [19659199] 0
"fin" : 9
"corps" : [
{
"type" [19659008]: "VariableDeclaration"
"start" : 0
"end" : 9 [1965999] 19659209] "déclarations" : [
{
"type" : "VariableDéclarator"
"start" : 4
"fin" : 9
"id" : {
"type" : "Identifiant"
"début" : 4
"fin" : 5
"nom" [nom] [[19659008]: "a"
}
"init" : {
"type" : [19659195] "Littéral"
"début" : 8
"fin" : 9
"valeur ": 2
" raw ": " 2 "
}
}
]
" kind ": " var "
}
]
" sourceType ": " module "
}
Essayons de créer sens de l'AST ci-dessus. C’est un objet JavaScript dont les propriétés sont de type début fin body et sourceType . start est l'index du premier caractère et end est la longueur de votre code, qui est var a = 2
dans ce cas. body contient la définition du code. C'est un tableau avec un seul objet puisqu'il n'y a qu'une seule instruction du type VariableDeclaration
dans notre programme. À l'intérieur VariableDeclaration
il spécifie l'identificateur a
et sa valeur initiale est 2
. Vérifiez les objets id
et init
. Le type de déclaration est var
. Cela peut également être let
ou const
.
Voyons un autre exemple pour mieux comprendre les AST:
function foo ( ) {
let bar = 2
retour bar
}
Et son AST est comme suit –
{
"type" : "Programme"
"start" : 0
"end" : 50
"body" : [
{
type ": " FonctionDéclaration "
" début ": 0
" fin ": 50 ,
"id" : {
"type" : "identificateur"
"start" : 9 [19659008]
"fin" : 12
"nom" : "foo"
}
"expression" ": false
" générateur ": false
" params ": []. 19659008]
"body" : {
"type" : "BlockStatement"
"start" [début]. 19659008]: 16
"end" : 50
"corps" : [
[
"type" : "VariableDeclaration"
"début" : 22
"fin" : 33 [19659008]
"déclarations" : [
{
"type" : "VariableDeclarator"
"début" . : 26
"end" : 33
"id" : {
"type" : "Identificateur"
"début" : 26
"fin" : 29 [196594000] "nom" : "bar"
}
"init" : {
"type" : "Literal "
" début ": 32
" fin ": 33
" valeur "[19659008]: 2
"raw" : "2"
"
] : " [genre19659008]: "let"
}
{
"type" : "ReturnStatement"
"start" : 38
"end" : 48
"argument" : {
"type" : "Identificateur"
"début" : 45
"fin" : 48
"nom" : "bar"
}
}
]
}
}
]
"sourceType" : "module"
}
Encore une fois, il a des propriétés – de type début fin corps ] et sourceType . start vaut 0, ce qui signifie que le premier caractère est à la position 0, et end vaut 50, ce qui signifie que la longueur du code est 50. body est un tableau avec un objet du type FunctionDeclaration
. Le nom de la fonction foo
est spécifié dans l'objet id
. Cette fonction ne prend aucun argument, donc params est un tableau vide. Le corps de la FunctionDeclaration
est de type BlockStatement
. BlockStatement
identifie la portée de la fonction. Le corps du BlockStatement
a deux objets pour VariableDeclaration
et ReturnStatement
. VariableDeclaration
est identique à l'exemple précédent. ReturnStatement
contient un argument portant le nom bar
car bar
est renvoyé par la fonction foo
.
C'est ce qu'il est. C'est comment les AST sont générés. Quand j'ai entendu parler d'AST pour la première fois, j'ai pensé à eux comme de gros arbres effrayants aux nœuds compliqués. Mais maintenant que nous connaissons bien les AST, ne pensez-vous pas qu'il ne s'agit que d'un groupe de nœuds bien conçus représentant la sémantique d'un programme?
Parser prend également en charge Scopes.
let globalVar = 2
function foo () {
let globalVar (19659007) = 3
console . log ( 'globalVar' globalVar )
}
La fonction foo
est destinée à l'impression 3 et non 2 car la valeur de globalVar
dans son étendue est de 3. Lors de l'analyse syntaxique du code JavaScript, l'analyseur génère également les étendues correspondantes.
Lorsqu'un globalVar
est référencé dans une fonction foo
nous commençons par rechercher globalVar
dans le périmètre fonctionnel. Si cette variable n'est pas trouvée dans l'étendue fonctionnelle, nous recherchons son parent, qui est dans ce cas l'objet global . Prenons un autre exemple:
let globalVar = 2
function foo () {
let localVar = 3
console . log ( 'localVar' localVar )
console . log ( 'globalVar' globalVar )
}
console . log ( 'localVar' localVar )
console . log ( 'globalVar' globalVar )
Les instructions de la console dans la fonction foo
seraient imprimées 3 et 2 alors que les instructions de la console en dehors de la fonction foo
afficheraient undefined et 3. En effet, localVar
n'est pas accessible en dehors de la fonction foo
. Il est défini dans le domaine d'application de la fonction foo
et une recherche de localVar
à l'extérieur de ce résultat donne undefined .
L'analyse syntaxique dans V8
V8 utilise deux analyseurs syntaxiques pour analyser le code JavaScript, appelés analyseur et pré-analyseur. Pour comprendre la nécessité de deux analyseurs, considérons le code ci-dessous:
function foo () {
console . log ( 'Je suis dans la fonction foo' )
}
function bar () [) 19659207] {
console . log ( "Je suis dans la barre de fonctions" )
}
/ * Fonction d’appel foo * /
foo ()
Lorsque le code ci-dessus est analysé, l'analyseur génère un AST représentant la fonction foo et la fonction bar . Cependant, la fonction bar n'est appelée nulle part dans le programme. Nous passons du temps à analyser et à compiler des fonctions qui ne sont pas utilisées, du moins lors du démarrage. bar peut être appelé ultérieurement, par exemple en cliquant sur un bouton. Mais ce n'est clairement pas nécessaire lors du démarrage. Peut-on gagner ce temps en ne compilant pas la fonction bar lors du démarrage? Oui, nous le pouvons!
L’analyseur est ce que nous faisons jusqu’à présent. Il analyse tout votre code, construit des AST, étend des portées et trouve toutes les erreurs de syntaxe. Le pré-analyseur est comme un analyseur rapide. Il ne compile que ce qui est nécessaire et ignore les fonctions qui ne sont pas appelées. Il construit des portées mais ne construit pas d’AST. Il ne trouve qu'un ensemble limité d'erreurs et est environ deux fois plus rapide que l'analyseur. La V8 utilise une approche heuristique pour déterminer la technique d'analyse au moment de l'exécution.
Prenons un exemple pour comprendre comment la V8 analyse le code JavaScript:
( function foo ( ) {
console . log ( "Je suis une fonction IIFE" )
fonction bar () {
console . log ( "Je suis une fonction interne de IIFE" )
}
) ( )
Lorsque l’analyseur trouve la parenthèse initiale, il comprend qu’il s’agit d’un IIFE et s’appelle immédiatement, il analyse donc la fonction foo
en utilisant un analyseur complet ou un analyseur syntaxique enthousiaste. À l’intérieur foo
lorsqu’il rencontre la fonction bar
il analyse ou prépare paresseusement la fonction bar
car, en se basant sur ses heuristiques, il sait que le La fonction bar
ne sera pas appelée immédiatement. Lorsque la fonction foo
est complètement analysée, V8 construit son AST ainsi que des oscilloscopes sans créer d’AST pour la fonction bar
. Il ne construit que des portées pour la fonction bar
.
Avez-vous déjà rencontré ce problème en écrivant le code JavaScript:
function toBeCalled () [}
toBeCalled ()
La fonction de BeCalled
est analysée paresseusement par le moteur V8, qui utilise alors un analyseur complet pour le faire fonctionner. Le temps passé à analyser paresseusement la fonction toBeCalled
est en réalité une perte de temps. La V8 analyse paresseusement la fonction à BeCalled
elle ne sait pas que la déclaration immédiate serait un appel à cette fonction. Pour éviter cela, vous pouvez indiquer à V8 quelles fonctions doivent être analysées (analyse complète).
( fonction àBeCalled () {[19659008]} )
àBeCalled ()
Le fait d'envelopper une fonction entre parenthèses indique à V8 que cette fonction doit être analysée avec précaution. Vous pouvez également ajouter un point d'exclamation avant la déclaration de fonction pour indiquer à V8 d'analyser avec avidité cette fonction.
! function de BeCalled () {[19659008]}
àBeCalled ()
Analyse des fonctions internes
function outer () {
function inner [19659207] () {}
}
Dans ce cas, le V8 analyse paresseusement les deux fonctions, external
et intérieure
. Lorsque nous appelons outer
la fonction outer
est analysée avec minutie / minutieusement et la fonction inside
est à nouveau analysée paresseusement. Cela signifie que la fonction intérieure
est analysée deux fois paresseusement. La situation est encore pire lorsque les fonctions sont fortement imbriquées.
function outer () {
function inner () 19659207] {
function insideInner () {}
}
retour interne
}
Initialement, les trois fonctions extérieures
intérieures
et à l'intérieur d'Inner
sont paresseusement analysées.
let innerFn = outer ()
innerFn ()
Lorsque nous appelons la fonction outer
elle est entièrement analysée et Les fonctions intérieure
et à l'intérieur intérieure
sont analysées paresseusement. Maintenant, lorsque nous appelons intérieure
intérieure
est entièrement analysée et intérieure: Inner
est analysée paresseusement. Cela fait que insideInner
soit analysé trois fois. N'utilisez pas de fonctions imbriquées quand elles ne sont pas nécessaires. Utilisez correctement les fonctions imbriquées!
Analyse syntaxique des fermetures
( function Extérieur () {
Laisser de [19459354] a = 2
let b = 3
fonction intérieure () {
retour a
}
retour intérieur
} )
Dans l'extrait de code ci-dessus, la fonction outer
étant entourée de parenthèses, elle est analysée avec impatience. La fonction intérieure
est analysée paresseusement. inner
renvoie la variable a, qui entre dans le cadre de sa fonction outer
. Il s'agit d'un cas valable de fermeture.
let innerFn = outer ()
innerFn () innerFn
renvoie très bien une valeur de 2 puisqu'il a accès à la variable a de sa portée parente. Lors de l'analyse de la fonction intérieure
lorsque V8 rencontre la variable a, il recherche la variable a dans le contexte de la fonction intérieure
. Puisque a n'est pas présent dans le domaine de inner
il le vérifie dans le domaine de la fonction outer
. V8 comprend que la variable a doit être enregistrée dans le contexte de la fonction et doit être conservée même après l'exécution de la fonction outer
. Ainsi, la variable a est stockée dans le contexte de fonction de outer
et est conservée jusqu'à l'exécution complète de sa fonction dépendante inner
. Veuillez noter que la variable b n'est pas conservée dans ce cas car elle n'est utilisée dans aucune des fonctions internes.
Lorsque nous appelons la fonction innerFn
la valeur de a n'est pas trouvée dans la pile d'appels, nous recherchons ensuite sa valeur dans le contexte de la fonction. Les recherches dans le contexte d'une fonction sont coûteuses comparées aux recherches dans la pile d'appels.
Vérifions le code analysé généré par V8.
function fnCalled () {1965}
console.log('Inside fnCalled')
}
function fnNotCalled () {
console.log('Inside fnNotCalled')
}
fnCalled()
As per our understanding, both of these functions will be lazily parsed and when we make a function call to fnCalled
it would be fully parsed and print Inside fnCalled. Let’s see this in action. Run the file containing the above code as node --trace_parse parse.js
. If you’ve cloned my GitHub repositoryyou’ll find this file under public/js folder. parse.js
is the name of the file, and --trace_parse
serves as an indicator to the runtime of nodejs to print the parsed output. This command would generate a dump of parsing logs. I’ll save the output of this command in a file parsedOutput.txt. For now, all that makes sense is the below screenshot of the dump.
dump.
Script Streaming
Now that we know how parsing works in V8, let’s understand one concept related to Script Streaming. Script Streaming is effective from Chrome version 41.
From what we’ve learned till now, we know it’s the main thread that parses the JavaScript code (even with async and defer keywords). With Script Streaming in place, now the parsing can happen in another thread. While the script is still getting downloaded by the main thread, the parser thread can start parsing the script. This means that the parsing would be completed in line with the download. This technique proves very helpful for large scripts and slow network connections. Check out the below image to understand how the browser operates with Script Streaming and without Script Streaming.
Kendo UI for jQuery - our complete UI component library that allows you to quickly build high-quality, responsive apps. It includes all the components you’ll need, from grids and charts to schedulers and dials.
Comments are disabled in preview mode.
Source link