1. Pourquoi JavaScript est-il monothread ?
Une fonctionnalité majeure du langage JavaScript est le monothreading, 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 une fonctionnalité essentielle du 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 ne doivent pas faire fonctionner le DOMAINE. Par conséquent, cette nouvelle norme ne modifie pas la nature monothread de JavaScript.
2. File d'attente des tâches
Un seul thread signifie que toutes les tâches doivent être mises en file d'attente et que la tâche suivante ne sera pas exécutée tant que la tâche précédente n'est pas 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 (comme les opérations Ajax pour lire les données du réseau), il faut attendre les résultats avant de continuer.
Les concepteurs du langage JavaScript ont réalisé qu'à ce stade, le processeur 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.
En conséquence, 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. C'est ce qu'on appelle l'exécution synchrone ; avec des temps d'attente longs, traitez d'abord les tâches suivantes, ce que l'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, puisqu'elle peut être considérée comme une exécution asynchrone sans tâche asynchrone.)
(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 thread 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". Il s'agit du mécanisme d'exécution de JavaScript. Ce processus ne cesse de se répéter.
3. Événements et fonctions de rappel
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, un événement est ajouté à la « file d'attente des tâches » pour indiquer que les tâches asynchrones associées peuvent entrer. 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.
4. Boucle d'événement
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, et elles ajoutent divers événements (clic, chargement, etc.) à la "tâche". file d'attente". terminé). 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.
5. Minuterie
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 seront exécutés 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ée à 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.
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).
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 requestAnimationFrame() 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 exécute la fonction de rappel qu'il spécifie. 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().
6. Boucle d'événement de Node.js
Node.js 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 (par @BusyRich).
D'après l'image ci-dessus, le mécanisme de fonctionnement de Node.js 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.
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 de la « file d’attente des 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 lise la "file d'attente des tâches" la prochaine fois. Autrement dit, la tâche spécifiée se produit toujours avant toutes les tâches asynchrones. La méthode setImmediate déclenche la fonction de rappel à la fin de la "file d'attente des tâches" actuelle, c'est-à-dire que la tâche qu'elle spécifie est toujours exécutée la prochaine fois que le thread principal lit la "file d'attente des tâches", ce qui est très similaire à setTimeout( fn, 0) . Voir l'exemple ci-dessous (via StackOverflow).
setTimeout(fonction timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// DÉLAI D'ATTENTE
Maintenant, regardez setImmediate.
setTimeout(fonction timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// DÉLAI D'ATTENTE
// 2
Dans le code ci-dessus, il y a deux setImmediate. Le premier setImmediate spécifie que la fonction de rappel A est déclenchée à la fin de la « file d'attente des tâches » actuelle (la « boucle d'événements » suivante) ; puis, setTimeout spécifie également que le délai d'attente de la fonction de rappel est déclenché à la fin de la « tâche » actuelle ; file d'attente", donc dans le résultat de sortie, TIMEOUT FIRED se classe derrière 1. Quant au classement 2 derrière TIMEOUT FIRED, c'est à cause d'une autre fonctionnalité importante de setImmediate : une "boucle d'événement" ne peut déclencher qu'une seule fonction de rappel spécifiée par setImmediate.
Nous obtenons une différence importante : plusieurs instructions process.nextTick sont toujours exécutées en même temps, tandis que plusieurs instructions setImmediate doivent être exécutées plusieurs fois. En fait, c'est la raison pour laquelle Node.js version 10.0 a ajouté la méthode setImmediate, sinon l'appel récursif à process.nextTick comme celui-ci sera sans fin et le thread principal ne lira pas du tout la "file d'attente des événements" !