1. Pourquoi JavaScript est-il unique?
Une caractéristique majeure du langage JavaScript est le threading unique, ce qui signifie que vous ne pouvez faire qu'une seule chose en même temps. Alors, pourquoi JavaScript ne peut-il pas avoir plusieurs threads? Cela améliorera l'efficacité.
Le threading unique de JavaScript est lié à son objectif. En tant que langage de script de navigateur, le but principal de JavaScript est d'interagir avec les utilisateurs et d'exploiter DOM. Cela détermine qu'il ne peut être qu'un seul thread, sinon il 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 sur un certain nœud DOM, et l'autre thread supprime ce nœud, quel thread le navigateur devrait-il prendre en ce moment?
Par conséquent, afin d'éviter la complexité, JavaScript est un seul fil de sa naissance, qui est devenu la caractéristique centrale de cette langue et ne changera pas à l'avenir.
Afin d'utiliser la puissance de calcul des CPU multi-core, HTML5 a proposé la norme du travailleur Web, permettant aux scripts JavaScript de créer plusieurs threads, mais les threads enfants sont complètement contrôlés par le thread principal et ne peuvent pas utiliser le DOM. Par conséquent, cette nouvelle norme ne modifie pas la nature du threading unique JavaScript.
2. Fitre de tâches
Le threading unique signifie que toutes les tâches doivent être en file d'attente et que la tâche précédente sera exécutée avant que la prochaine tâche ne soit exécutée. Si la tâche précédente prend beaucoup de temps, la prochaine tâche doit attendre.
Si la file d'attente est due à la grande quantité de calcul et que le CPU est trop occupé, ce serait bien, mais plusieurs fois, le CPU est inactif car le périphérique IO (périphérique d'entrée et de sortie) est très lent (comme l'opération Ajax lit les données du réseau), et que vous devez attendre le résultat à sortir avant de les exécuter.
Le concepteur de la langue javascript s'est rendu compte qu'à ce moment, le CPU pouvait ignorer complètement l'appareil IO, suspendre les tâches d'attente et exécuter les tâches suivantes en premier. Attendez que le périphérique IO renvoie le résultat, puis retournez et continuez la tâche suspendue.
Par conséquent, JavaScript a deux méthodes d'exécution: l'une est que le CPU s'exécute en séquence, la tâche précédente se termine, puis la tâche suivante est exécutée, qui est appelée exécution synchrone; L'autre est que le CPU saute les tâches avec un long temps d'attente et traite d'abord les tâches suivantes, qui est appelée exécution asynchrone. Les programmeurs choisissent indépendamment le type de méthode d'exécution à adopter.
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 pour former une pile de contexte d'exécution.
(2) En plus du thread principal, il existe également une "file d'attente de tâches". Le système place les tâches asynchrones dans la "file d'attente des tâches", puis continue d'exécuter des tâches suivantes.
(3) Une fois que toutes les tâches de la "pile d'exécution" seront exécutées, le système lira la "file d'attente de tâches". Si pour le moment, la tâche asynchrone a mis fin à l'état d'attente, il entrera la pile d'exécution de la "file d'attente de tâche" et reprendra l'exécution.
(4) Le fil principal continue de répéter la troisième étape au-dessus.
La figure suivante 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". Il s'agit du mécanisme de course de JavaScript. Ce processus sera répété en continu.
3. Événements et fonctions de rappel
"La file d'attente des tâches" est essentiellement une file d'attente d'événements (également comprise comme une file d'attente de messages). Lorsqu'un périphérique IO termine une tâche, il ajoute un événement à la "file d'attente de tâche", indiquant que les tâches asynchrones pertinentes peuvent entrer la "pile d'exécution". Le fil principal lit la «file d'attente des tâches», ce qui signifie lire les événements à l'intérieur.
Les événements de la "file d'attente de tâches" incluent des événements en plus des événements des appareils IO, mais également des événements générés par les utilisateurs (tels que les clics de souris, le défilement de la 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 produisent et attendront la lecture du fil principal.
Le soi-disant "rappel" est le code qui sera accroché par le fil principal. Les tâches asynchrones doivent spécifier une fonction de rappel. Lorsque la tâche asynchrone revient de la "file d'attente de tâche" à la pile d'exécution, la fonction de rappel sera exécutée.
"Task Fitre" est une structure de données de premier entrée, avec des événements classés en premier et qui sont préférés pour revenir au fil principal. Le processus de lecture du fil principal est essentiellement automatique. Tant que la pile d'exécution est effacée, le premier événement de la "file d'attente de tâche" reviendra automatiquement au thread principal. Cependant, en raison de la fonction "temporaire" 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.
4. Boucle d'événement
Le fil principal lit les événements de la "file d'attente des tâches". Ce processus boucle en continu, de sorte que l'ensemble du mécanisme de course est également appelé boucle d'événement.
Pour mieux comprendre l'événement Loop, veuillez consulter l'image ci-dessous (citée dans le discours de Philip Roberts "Aide, je suis coincé dans une boucle d'événements").
Dans la figure ci-dessus, lorsque le fil principal est en cours d'exécution, il génère du tas et de la pile. Le code de la pile appelle diverses API externes, qui ajoutent divers événements (cliquez, chargez, terminé) à la "file d'attente de tâche". 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 à leur tour.
Exécutez le code dans la pile, toujours exécuté avant de lire la "file d'attente de tâche". Veuillez consulter l'exemple suivant.
La copie de code est la suivante:
var req = new xmlHttpRequest ();
req.open («get», URL);
req.onload = function () {};
req.onError = function () {};
req.send ();
La méthode de req.Send dans le code ci-dessus est une opération AJAX pour envoyer des données au serveur. Il s'agit d'une tâche asynchrone, ce qui signifie que le système lira la "file d'attente de tâche" uniquement après que tout le code du script actuel soit exécuté. Il est donc équivalent à la méthode d'écriture suivante.
La copie de code est la suivante:
var req = new xmlHttpRequest ();
req.open («get», URL);
req.send ();
req.onload = function () {};
req.onError = function () {};
C'est-à-dire que les parties de la fonction de rappel spécifiée (onload et onerror) ne sont pas importantes avant ou après la méthode Send (), car elles font partie de la pile d'exécution, et le système les exécutera toujours avant de lire la "file d'attente de tâche".
5. minuteur
En plus de placer des tâches asynchrones, la "file d'attente des tâches" a également une autre fonction, qui consiste à placer des événements chronométrés, c'est-à-dire à spécifier la durée de l'exécution du code. C'est ce qu'on appelle la fonction "temporaire", qui est le code exécuté régulièrement.
La fonction temporaire est principalement remplie par deux fonctions: setTimeout () et setInterval (). Leurs mécanismes de course internes sont exactement les mêmes. La différence est que le code spécifié par le premier est exécuté à un moment donné, tandis que le second est exécuté à plusieurs reprises. Ce qui suit discute principalement Settimeout ().
SetTimeout () accepte deux paramètres, le premier est la fonction de rappel, et le second est le nombre de millisecondes pour reporter l'exécution.
La copie de code est la suivante:
console.log (1);
setTimeout (function () {console.log (2);}, 1000);
console.log (3);
Le résultat d'exécution du code ci-dessus est de 1, 3, 2, car SetTimeout () retarde la deuxième ligne jusqu'à 1000 millisecondes.
Si le deuxième paramètre de setTimeout () est défini sur 0, cela signifie que la fonction de rappel spécifiée (intervalle de 0 milliseconde) est exécutée immédiatement après l'exécution du code actuel (la pile d'exécution est effacée).
La copie de code est la suivante:
setTimeout (function () {console.log (1);}, 0);
console.log (2);
Les résultats d'exécution du code ci-dessus sont toujours 2 et 1, car le système exécutera la fonction de rappel dans la "file d'attente de tâches" uniquement après l'exécution de la deuxième ligne.
La norme HTML5 spécifie que la valeur minimale (intervalle le plus court) du deuxième paramètre de setTimeout () ne doit pas être inférieure à 4 millisecondes. S'il est inférieur à cette valeur, il augmentera automatiquement. Avant cela, les navigateurs plus anciens fixent l'intervalle minimum à 10 millisecondes.
De plus, pour ces modifications DOM (en particulier les pièces impliquant une rediffusion de pages), elles 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 demandeanimationframe () est meilleur que setTimeout ().
Il convient de noter que setTimeout () insère simplement l'événement dans la "file d'attente des tâches". Vous devez attendre que le code actuel (pile d'exécution) soit exécuté avant que le thread principal exécute la fonction de rappel qu'il spécifie. Si le code actuel prend beaucoup de temps, il peut prendre beaucoup de temps pour attendre, il n'y a donc aucune garantie que la fonction de rappel sera exécutée au moment spécifié par setTimeout ().
6. Boucle d'événement Node.js
Node.js est également une boucle d'événement unique, mais son mécanisme de course est différent de celui de l'environnement du navigateur.
Veuillez consulter le diagramme ci-dessous (auteur @busyrich).
Selon la figure ci-dessus, le mécanisme de course de Node.js est le suivant.
(1) V8 Parses Parses JavaScript Scripts.
(2) Le code analysé appelle l'API du nœud.
(3) La bibliothèque Libuv est responsable de l'exécution de l'API de nœud. Il attribue différentes tâches à différents threads, forme une boucle d'événement et renvoie les résultats d'exécution de la tâche au moteur V8 de manière asynchrone.
(4) Le moteur V8 renvoie le résultat à l'utilisateur.
En plus des deux méthodes Settimeout et SetInterval, Node.js fournit également deux autres méthodes liées à la «file d'attente des tâches»: process.NextTick et SetImMediate. Ils peuvent nous aider à approfondir notre compréhension des «files d'attente de tâches».
La méthode process.nexttick peut déclencher la fonction de rappel à la fin de la "pile d'exécution" actuelle avant que le thread principal ne lit la "file d'attente de tâche" la prochaine fois. C'est-à-dire que les tâches qu'il spécifie se produit toujours avant toutes les tâches asynchrones. La méthode SetImMediate déclenche la fonction de rappel à la queue de la "file d'attente de tâche" actuelle, c'est-à-dire que la tâche qu'il spécifie est toujours exécutée la prochaine fois que le thread principal lira la "file d'attente de tâche", qui est très similaire à Settimeout (FN, 0). Veuillez consulter l'exemple suivant (via Stackoverflow).
La copie de code est la suivante:
process.NextTick (fonction a () {
console.log (1);
process.NextTick (fonction b () {console.log (2);});
});
setTimeout (fonction timeout () {
Console.log («Timeout Fired»);
}, 0)
// 1
// 2
// Timeout tiré
Dans le code ci-dessus, puisque la fonction de rappel spécifiée par la méthode process.nexttick est toujours déclenchée à la queue de la "pile d'exécution" actuelle, non seulement la fonction a est exécutée d'abord que le délai d'expiration de la fonction de rappel spécifié par setTimeout, mais la fonction B est également exécutée en premier que le délai d'attente. Cela signifie que s'il existe plusieurs instructions Process.NextTick (qu'ils soient imbriqués ou non), ils seront tous exécutés sur la "pile d'exécution" actuelle.
Maintenant, regardons Settimmediate.
La copie de code est la suivante:
SetImMediate (fonction a () {
console.log (1);
SetImMediate (fonction b () {console.log (2);});
});
setTimeout (fonction timeout () {
Console.log («Timeout Fired»);
}, 0)
// 1
// Timeout tiré
// 2
Dans le code ci-dessus, il y a deux définies. Le premier SetImMediate spécifie que la fonction de rappel a est déclenchée à la queue de la "file d'attente de tâche" actuelle (la "boucle d'événement" suivante); Ensuite, Settimeout spécifie également que le délai d'expiration de la fonction de rappel est déclenché à la queue de la "file d'attente de tâches" actuelle, donc dans le résultat de sortie, le délai d'expiration est classé derrière 1. Comme pour le classement des 2 derrière le délai d'expiration, c'est parce qu'une autre caractéristique importante de Settimediate: une "boucle d'événement" ne peut que déclencher une fonction de rappel spécifiée par setimediate.
Nous avons obtenu une différence importante à partir de ceci: plusieurs instructions Process.NextTick sont toujours exécutées en même temps, tandis que plusieurs SETMediates nécessitent plusieurs fois pour être exécutés. En fait, c'est exactement pourquoi Node.js version 10.0 ajoute la méthode SETIMMEDIATE. Sinon, l'appel récursif à procéder.Nexttick comme les suivants sera sans fin, et le fil principal ne lira pas du tout la "file d'attente d'événements"!
La copie de code est la suivante:
process.NextTick (fonction foo () {
process.NextTick (FOO);
});
En fait, maintenant, si vous écrivez un processus récursif.Nexttick, Node.js lancera un avertissement vous demandant de changer pour setimeDiate.
De plus, puisque la fonction de rappel spécifiée par Process.NextTtick est déclenchée dans cette "boucle d'événements" et Settimmediate spécifie qu'elle est déclenchée dans la "boucle d'événement" suivante, il est évident que le premier se produit toujours plus tôt que le dernier et est également plus efficace dans l'exécution (car il n'est pas nécessaire de vérifier la "file d'attente de tâche").