Il y a un an, j'écrivais un article « Qu'est-ce qu'une boucle d'événement ? » ", a parlé de ma compréhension d'Event Loop.
Le mois dernier, j'ai vu le discours de Philip Roberts "Au secours, je suis coincé dans une boucle événementielle". Ce n’est qu’à ce moment-là que j’ai réalisé avec embarras que ma compréhension était fausse. J'ai décidé de réécrire cette question pour décrire le fonctionnement interne du moteur JavaScript en détail, complètement et correctement. Ci-dessous ma réécriture.
Avant de saisir le texte principal, insérez un message. Mon nouveau livre "Introduction à ECMAScript 6" a été publié (page de copyright, page intérieure 1, page intérieure 2). Il est imprimé en couleur sur papier couché et est également accompagné d'un index (bien sûr le prix). est un peu plus cher que des livres similaires). Pour prévisualiser et acheter, cliquez ici.
Une caractéristique majeure du langage JavaScript est qu'il est monothread, ce qui signifie qu'il ne peut faire qu'une seule chose à la fois. Alors pourquoi JavaScript ne peut-il pas avoir plusieurs threads ? Cela peut améliorer l’efficacité.
Le fil de discussion unique de JavaScript est lié à son objectif. En tant que langage de script de navigateur, l'objectif principal de JavaScript est d'interagir avec les utilisateurs et de manipuler le DOM. Cela détermine qu'il ne peut être qu'un seul thread, sinon cela entraînera des problèmes de synchronisation très complexes. Par exemple, supposons que JavaScript ait deux threads en même temps. Un thread ajoute du contenu à un certain nœud DOM et l'autre thread supprime le nœud. Dans ce cas, quel thread le navigateur doit-il utiliser ?
Par conséquent, afin d'éviter toute complexité, JavaScript est monothread depuis sa naissance. Cela est devenu la fonctionnalité principale de ce langage et ne changera pas à l'avenir.
Afin de profiter de la puissance de calcul des CPU multicœurs, HTML5 propose le standard Web Worker, qui permet aux scripts JavaScript de créer plusieurs threads, mais les threads enfants sont entièrement contrôlés par le thread principal et sont pas autorisé à exploiter le DOM. Par conséquent, cette nouvelle norme ne modifie pas la nature monothread de JavaScript.
Un seul thread signifie que toutes les tâches doivent être mises en file d'attente et que la tâche suivante ne sera exécutée que lorsque la tâche précédente sera terminée. Si la tâche précédente prend beaucoup de temps, la tâche suivante devra attendre.
Si la file d'attente est due à une grande quantité de calculs et que le CPU est trop occupé, oubliez ça, mais souvent le CPU est inactif car le périphérique IO (périphérique d'entrée et de sortie) est très lent (par exemple , opérations Ajax lues sur le réseau (obtenir des données), vous devez attendre que les résultats sortent avant de continuer.
Les concepteurs du langage JavaScript ont réalisé qu'à ce stade, le CPU peut ignorer complètement le périphérique IO, suspendre les tâches en attente et exécuter les tâches ultérieures en premier. Attendez que le périphérique IO renvoie le résultat, puis revenez en arrière et poursuivez l'exécution de la tâche suspendue.
Par conséquent, JavaScript a deux méthodes d'exécution : l'une est que le CPU l'exécute en séquence, la tâche précédente se termine, puis la tâche suivante est exécutée, appelée exécution synchrone ; l'autre est que le CPU saute ; le temps d'attente Pour les tâches longues, les tâches suivantes sont traitées en premier. C'est ce qu'on appelle l'exécution asynchrone. C'est au programmeur de choisir la méthode d'exécution à utiliser.
Plus précisément, le mécanisme de fonctionnement de l'exécution asynchrone est le suivant. (Il en va de même pour l'exécution synchrone, car elle peut être considérée comme une exécution asynchrone sans tâches asynchrones.)
(1) Toutes les tâches sont exécutées sur le thread principal, formant une pile de contexte d'exécution .
(2) En plus du fil de discussion principal, il existe également une "file d'attente des tâches". Le système place les tâches asynchrones dans la « file d'attente des tâches », puis continue d'exécuter les tâches suivantes.
(3) Une fois que toutes les tâches de la « pile d'exécution » ont été exécutées, le système lira la « file d'attente des tâches ». Si à ce moment-là, la tâche asynchrone a mis fin à l'état d'attente, elle entrera dans la pile d'exécution à partir de la « file d'attente des tâches » et reprendra l'exécution.
(4) Le fil principal continue de répéter la troisième étape ci-dessus.
L'image ci-dessous est un diagramme schématique du thread principal et de la file d'attente des tâches.
Tant que le thread principal est vide, il lira la "file d'attente des tâches". C'est le mécanisme d'exécution de JavaScript. Ce processus ne cesse de se répéter.
La "file d'attente des tâches" est essentiellement une file d'attente d'événements (peut également être comprise comme une file d'attente de messages). Lorsque le périphérique IO termine une tâche, il s'agit d'une file d'attente d'événements. dans la "file d'attente des tâches" Ajoutez un événement à la "file d'attente" pour indiquer que la tâche asynchrone associée peut entrer dans la "pile d'exécution". Le thread principal lit la "file d'attente des tâches", ce qui signifie lire les événements qu'elle contient.
Les événements de la "file d'attente des tâches", en plus des événements du périphérique IO, incluent également certains événements générés par l'utilisateur (tels que les clics de souris, le défilement de page, etc.). Tant que la fonction de rappel est spécifiée, ces événements entreront dans la « file d'attente des tâches » lorsqu'ils se produiront, en attendant que le thread principal les lise.
La soi-disant "fonction de rappel" est le code qui sera suspendu par le thread principal. Les tâches asynchrones doivent spécifier une fonction de rappel. Lorsque la tâche asynchrone revient de la « file d'attente des tâches » vers la pile d'exécution, la fonction de rappel sera exécutée.
La "file d'attente des tâches" est une structure de données premier entré, premier sorti. Les événements classés en premier sont renvoyés en premier au thread principal. Le processus de lecture du thread principal est fondamentalement automatique. Dès que la pile d'exécution est effacée, le premier événement de la « file d'attente des tâches » reviendra automatiquement au thread principal. Cependant, en raison de la fonction "timer" mentionnée plus loin, le thread principal doit vérifier le temps d'exécution et certains événements doivent revenir au thread principal à l'heure spécifiée.
Le thread principal lit les événements de la "file d'attente des tâches". Ce processus est cyclique, donc l'ensemble du mécanisme de fonctionnement est également appelé Event Loop ).
Afin de mieux comprendre Event Loop, veuillez regarder l'image ci-dessous (extraite du discours de Philip Roberts "Au secours, je suis coincé dans une boucle d'événement").
Dans l'image ci-dessus, lorsque le thread principal est en cours d'exécution, un tas et une pile sont générés. Le code dans la pile appelle diverses API externes. Elles se trouvent dans le ". tâche" Divers événements (clic, chargement, terminé) sont ajoutés à la file d'attente. Tant que le code de la pile est exécuté, le thread principal lira la « file d'attente des tâches » et exécutera les fonctions de rappel correspondant à ces événements dans l'ordre.
Le code dans la pile d'exécution est toujours exécuté avant de lire la "file d'attente des tâches". Jetez un œil à l’exemple ci-dessous.
var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function (){}; req.onerror = function (){}; req.send();Copier après la connexion
La méthode req.send dans le code ci-dessus est une opération Ajax pour envoyer des données au serveur. C'est une tâche asynchrone, ce qui signifie qu'elle n'est effectuée qu'après tous les codes de. le script en cours est exécuté, le système lira la "file d'attente des tâches". Cela équivaut donc à l’écriture suivante.
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function (){}; req.onerror = function (){};Copier après la connexion
C'est-à-dire que la partie qui spécifie la fonction de rappel (onload et onerror) n'a pas d'importance avant ou après la méthode send(), car elle appartient à une partie de la pile d'exécution. Le système lira toujours la "file d'attente des tâches" après les avoir exécutées.
En plus de placer des tâches asynchrones, la "file d'attente des tâches" a également une fonction, c'est-à-dire qu'elle peut placer des événements chronométrés, c'est-à-dire spécifier combien de temps certains codes sera exécuté après. C'est ce qu'on appelle la fonction « timer », qui est du code exécuté régulièrement.
La fonction timer est principalement complétée par les deux fonctions setTimeout() et setInterval(). Leurs mécanismes de fonctionnement internes sont exactement les mêmes. La différence est que le code spécifié par la première est exécuté une seule fois, tandis que la seconde. est exécuté à plusieurs reprises. Ce qui suit traite principalement de setTimeout().
setTimeout() accepte deux paramètres, le premier est la fonction de rappel et le second est le nombre de millisecondes pour retarder l'exécution.
console.log(1); setTimeout(function(){console.log(2);},1000); console.log(3);Copier après la connexion
Les résultats d'exécution du code ci-dessus sont 1, 3, 2, car setTimeout() retarde l'exécution de la deuxième ligne jusqu'à 1000 millisecondes plus tard.
Si le deuxième paramètre de setTimeout() est défini sur 0, cela signifie qu'une fois le code actuel exécuté (la pile d'exécution est effacée), la fonction de rappel spécifiée sera exécutée immédiatement (intervalle de 0 milliseconde).
setTimeout(function(){console.log(1);}, 0); console.log(2);Copier après la connexion
Le résultat de l'exécution du code ci-dessus est toujours 2, 1, car ce n'est qu'après l'exécution de la deuxième ligne que le système exécutera la fonction de rappel dans la "file d'attente des tâches" .
La norme HTML5 stipule que la valeur minimale (intervalle le plus court) du deuxième paramètre de setTimeout() ne doit pas être inférieure à 4 millisecondes. Si elle est inférieure à cette valeur, elle augmentera automatiquement. Avant cela, les anciens navigateurs fixaient l'intervalle minimum à 10 millisecondes.
De plus, ces modifications du DOM (en particulier celles impliquant un nouveau rendu de page) ne sont généralement pas exécutées immédiatement, mais toutes les 16 millisecondes. À l'heure actuelle, l'effet de l'utilisation de requestAnimFrame() est meilleur que celui de setTimeout().
Il convient de noter que setTimeout() insère uniquement l'événement dans la "file d'attente des tâches". Le thread principal doit attendre que le code actuel (pile d'exécution) ait fini de s'exécuter avant que le thread principal n'exécute la fonction de rappel qu'il a. précise. Si le code actuel prend beaucoup de temps, cela peut prendre beaucoup de temps, il n'y a donc aucun moyen de garantir que la fonction de rappel sera exécutée à l'heure spécifiée par setTimeout().
Explication détaillée du mécanisme de fonctionnement de JavaScript : reparlons dEvent Loop est également une boucle d'événement à thread unique, mais son mécanisme de fonctionnement est différent de l'environnement du navigateur.
Veuillez consulter le schéma ci-dessous (auteur @BusyRich).
Sur la base de l'image ci-dessus, le mécanisme de fonctionnement de Explication détaillée du mécanisme de fonctionnement de JavaScript : reparlons dEvent Loop est le suivant.
(1) Le moteur V8 analyse les scripts JavaScript.
(2) Le code analysé appelle l'API Node.
(3) La bibliothèque libuv est responsable de l'exécution de l'API Node. Il alloue différentes tâches à différents threads pour former une boucle d'événement (boucle d'événement), et renvoie les résultats d'exécution des tâches au moteur V8 de manière asynchrone.
(4) Le moteur V8 renvoie les résultats à l'utilisateur.
Explication détaillée du mécanisme de fonctionnement de JavaScript : reparlons dEvent Loop有一个process.nextTick()方法,可以将指定事件推迟到Event Loop的下一次执行,或者说放到"Explication détaillée du mécanisme de fonctionnement de JavaScript : reparlons dEvent Loop"的头部,也就是当前的执行栈清空之后立即执行。
function foo() { console.error(1); } process.nextTick(foo); console.log(2); // 2 // 1Copier après la connexion
process.nextTick(foo)的作用,与setTimeout(foo, 0)很相似,但是执行效率高得多。
以上就是JavaScript 运行机制详解:再谈Event Loop的内容,更多相关内容请关注PHP中文网(www.php.cn)!