Bonjour chers collègues. Il était une fois, 
un article sur Habré a été rédigé par John Rezig sur ce sujet. 10 ans se sont écoulés et le sujet doit encore être clarifié. Par conséquent, nous proposons aux personnes intéressées de lire l'article de Samer Buna, qui donne non seulement un aperçu théorique des minuteries en JavaScript (dans le contexte de Node.js), mais également des tâches à leur sujet.

Il y a quelques semaines, j'ai tweeté la question suivante à partir d'une seule interview:
«Où est le code source des fonctions setTimeout et setInterval? Où le chercheriez-vous? Vous ne pouvez pas le rechercher sur Google :) "
*** Répondez par vous-même, puis lisez la suite ***
Environ la moitié des réponses à ce tweet étaient incorrectes. Non, le cas N'EST PAS RELIÉ à la V8 (ou à d'autres VM) !!! Les fonctions telles que 
setTimeout et 
setInterval , fièrement appelées JavaScript JavaScript Timers, ne font partie d'aucune spécification ECMAScript ou implémentation de moteur JavaScript. Les fonctions de minuterie sont implémentées au niveau du navigateur, leur implémentation diffère donc selon les navigateurs. Les temporisateurs sont également implémentés nativement dans le runtime Node.js.
Dans les navigateurs, les principales fonctions de temporisation se réfèrent à l'interface 
Window , qui est également associée à d'autres fonctions et objets. Cette interface fournit un accès global à tous ses éléments dans le cadre principal de JavaScript. C'est pourquoi la fonction 
setTimeout peut être exécutée directement dans la console du navigateur.
Dans Node, les minuteries font partie de l'objet 
global , qui est conçu comme l'interface de navigateur de 
Window . Le code source des minuteries dans Node est affiché 
ici .
Il peut sembler à quelqu'un que c'est juste une mauvaise question de l'entretien - à quoi ça sert de savoir ça?! En tant que développeur JavaScript, je pense de cette façon: il est supposé que vous devez le savoir, car l'inverse peut indiquer que vous ne comprenez pas très bien comment V8 (et d'autres machines virtuelles) interagissent avec les navigateurs et Node.
Regardons quelques exemples et résolvons quelques tâches de minuterie, d'accord?
Vous pouvez utiliser la commande node pour exécuter les exemples de cet article. La plupart des exemples présentés ici figurent dans mon cours de mise en route de Node.js sur Pluralsight.Exécution de fonction différéeLes temporisateurs sont des fonctions d'ordre supérieur avec lesquelles vous pouvez retarder ou répéter l'exécution d'autres fonctions (le temporisateur reçoit une telle fonction comme premier argument).
Voici un exemple d'exécution différée:
 
Dans cet exemple, à l'aide de 
setTimeout le message d'accueil est retardé de 4 secondes. Le deuxième argument de 
setTimeout est le délai (en ms). Je multiplie 4 par 1000 pour obtenir 4 secondes.
Le premier argument de 
setTimeout est une fonction dont l'exécution sera retardée.
Si vous exécutez le fichier 
example1.js avec la commande node, Node se met en pause pendant 4 secondes, puis affiche un message de bienvenue (suivi d'une sortie).
Remarque: le premier argument de 
setTimeout n'est qu'une 
référence de fonction . Il ne doit pas s'agir d'une fonction intégrée, telle que 
example1.js . Voici le même exemple sans utiliser la fonction intégrée:
 const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000); 
Passer des argumentsSi la fonction pour laquelle 
setTimeout est utilisé pour retarder accepte des arguments, vous pouvez utiliser les arguments restants de la fonction 
setTimeout elle-même (après ceux que nous avons déjà étudiés) pour transférer les valeurs des arguments à la fonction différée.
 
Voici un exemple:
 
La fonction 
rocks ci-dessus, retardée de 2 secondes, prend l'argument 
who , et l'appel à 
setTimeout transmet la valeur "Node.js" comme tel argument 
who .
Lors de l'exécution de 
example2.js avec la commande 
node , la phrase «Node.js rocks» s'affiche au bout de 2 secondes.
Tâche n ° 1Ainsi, sur la base du matériel déjà étudié sur 
setTimeout , nous afficherons les 2 messages suivants après les délais correspondants.
- Le message «Bonjour après 4 secondes» s'affiche après 4 secondes.
- Le message «Bonjour après 8 secondes» s'affiche après 8 secondes.
LimitationDans votre solution, vous pouvez définir une seule fonction qui contient des fonctions intégrées. Cela signifie que de nombreux appels 
setTimeout devront utiliser la même fonction.
SolutionVoici comment je résoudrais ce problème:
 
Pour moi, 
theOneFunc reçoit l'argument 
delay et utilise la valeur de cet argument 
delay dans le message affiché à l'écran. Ainsi, la fonction peut afficher différents messages en fonction de la valeur de retard que nous lui communiquerons.
Ensuite, j'ai utilisé 
theOneFunc dans deux appels 
setTimeout , le premier appel étant déclenché après 4 secondes et le second après 8 secondes. Ces deux appels 
setTimeout reçoivent également un troisième argument, représentant l'argument de 
delay pour 
theOneFunc .
En exécutant le fichier 
solution1.js avec la commande node, nous afficherons les exigences de la tâche et le premier message apparaîtra après 4 secondes et le second après 8 secondes.
Répétez la fonctionMais que se passe-t-il si je vous demande d'afficher un message toutes les 4 secondes, pour une durée illimitée?
Bien sûr, vous pouvez 
setTimeout dans une boucle, mais l'API timer propose également la fonction 
setInterval , avec laquelle vous pouvez programmer l'exécution «éternelle» de toute opération.
Voici un exemple de 
setInterval :
 
Ce code affichera un message toutes les 3 secondes. Si vous exécutez 
example3.js avec la commande 
node , Node affichera cette commande jusqu'à ce que vous 
example3.js la fin du processus (CTRL + C).
Annuler les minuteursPuisqu'une action est affectée lorsque la fonction de temporisation est appelée, cette action peut également être annulée avant son exécution.
L'appel 
setTimeout renvoie un ID de temporisateur et vous pouvez utiliser cet ID de temporisateur lors de l'appel de 
clearTimeout pour annuler le temporisateur. Voici un exemple:
 
Ce temporisateur simple devrait se déclencher après 0 ms (c'est-à-dire tout de suite), mais cela ne se produira pas, car nous capturons la valeur de 
timerId et 
timerId immédiatement ce temporisateur en appelant 
clearTimeout .
Lors de l'exécution de 
example4.js avec la commande 
node , Node n'imprimera rien - le processus se terminera simplement immédiatement.
Soit dit en passant, Node.js fournit également un autre moyen de 
setTimeout avec une valeur de 0 ms. Il y a une autre fonction dans l'API de temporisation Node.js appelée 
setImmediate , et elle fait essentiellement la même chose que 
setTimeout avec une valeur de 0 ms, mais dans ce cas, vous pouvez omettre le délai:
 setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), ); 
La fonction 
setImmediate n'est 
setImmediate prise en charge dans tous les navigateurs . Ne l'utilisez pas dans le code client.
Parallèlement à 
clearTimeout il existe une fonction 
clearInterval qui fait de même, mais avec des appels 
setInerval , et il existe également un appel 
clearImmediate .
Timer Delay - une chose non garantieAvez-vous remarqué que dans l'exemple précédent, lors de l'exécution d'une opération avec 
setTimeout après 0 ms, cette opération ne se produit pas immédiatement (après 
setTimeout ), mais uniquement après que tout le code de script a été complètement exécuté (y compris l'appel 
clearTimeout )?
Permettez-moi de clarifier ce point avec un exemple. Voici un simple appel 
setTimeout qui devrait fonctionner en une demi-seconde - mais cela ne se produit pas:
 
Immédiatement après avoir défini la minuterie dans cet exemple, nous bloquons de manière synchrone l'environnement d'exécution avec une grande boucle 
for . La valeur de 
1e10 est 1 avec 10 zéros, donc le cycle dure 10 milliards de cycles de processeur (en principe, cela simule un processeur surchargé). Le nœud ne peut rien faire tant que cette boucle n'est pas terminée.
Bien sûr, en pratique, c'est très mauvais, mais cet exemple aide à comprendre que le délai 
setTimeout n'est pas garanti, mais plutôt la 
valeur minimale . Une valeur de 500 ms signifie que le retard durera au moins 500 ms. En fait, le script mettra beaucoup plus de temps pour afficher la ligne de bienvenue à l'écran. Il devra d'abord attendre la fin du cycle de blocage.
Problème des minuteries # 2Écrivez un script qui affichera le message «Hello World» une fois par seconde, mais seulement 5 fois. Après 5 itérations, le script devrait afficher un message «Terminé», après quoi le processus de nœud se terminera.
Limitation : lors de la résolution de ce problème, vous ne pouvez pas appeler 
setTimeout .
Astuce : besoin d'un compteur.
SolutionVoici comment je résoudrais ce problème:
 let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000); 
J'ai mis 0 comme valeur initiale de 
counter , puis appelé 
setInterval , qui prend son identifiant.
Une fonction différée affichera un message et augmentera à chaque fois le compteur d'un. À l'intérieur de la fonction différée, nous avons une instruction if, qui vérifiera si 5 itérations ont déjà passé. Après 5 itérations, le programme affiche «Terminé» et efface la valeur d'intervalle en utilisant la constante 
intervalId capturée. Le délai d'intervalle est de 1000 ms.
Qui appelle exactement les fonctions différées?Lorsque vous utilisez le JavaScript 
this dans une fonction régulière, comme ceci par exemple:
 function whoCalledMe() { console.log('Caller is', this); } 
la valeur du 
this correspondra à l' 
appelant . Si vous définissez la fonction ci-dessus dans le Node REPL, l'objet 
global l'appellera. Si vous définissez une fonction dans la console du navigateur, l'objet 
window l'appellera.
Définissons une fonction comme une propriété d'un objet pour le rendre un peu plus clair:
 const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } };  
Maintenant, lorsque nous utiliserons directement le lien vers celui-ci lors de l' 
obj.whoCallMe fonction 
obj.whoCallMe , l'objet 
obj (identifié par son 
id ) agira comme l'appelant:

Maintenant, la question est: qui sera l'appelant si vous passez le lien vers 
obj.whoCallMe à 
setTimetout ?
 
Qui est l'appelant dans ce cas?La réponse sera différente selon l'endroit où la fonction de minuterie est exécutée. Dans ce cas, la dépendance à l'égard de la personne qui appelle est tout simplement inacceptable. Vous perdrez le contrôle de l'appelant, car cela dépendra de l'implémentation du temporisateur qui dans ce cas appelle votre fonction. Si vous testez ce code dans un Node REPL, alors l'objet 
Timeout sera l'appelant:

Remarque: ceci n'est important que lorsque le JavaScript de 
this utilisé dans les fonctions normales. Lorsque vous utilisez les fonctions fléchées, l'appelant ne devrait pas vous déranger du tout.
Problème n ° 3Écrivez un script qui produira en continu un message «Hello World» avec des délais variables. Commencez avec un délai d'une seconde, puis augmentez-le d'une seconde à chaque itération. À la deuxième itération, le délai sera de 2 secondes. Le troisième - trois, et ainsi de suite.
Incluez un retard dans le message affiché. Vous devriez obtenir quelque chose comme ceci:
Hello World. 1
Hello World. 2
Hello World. 3
...Limitations : les variables ne peuvent être définies qu'en utilisant const. L'utilisation de let ou var ne l'est pas.
SolutionÉtant donné que la durée du délai dans cette tâche est une variable, vous ne pouvez pas utiliser 
setInterval ici, mais vous pouvez configurer manuellement l'exécution d'intervalle à l'aide de 
setTimeout dans un appel récursif. La première fonction exécutée avec 
setTimeout créera le temporisateur suivant, et ainsi de suite.
De plus, comme vous ne pouvez pas utiliser 
let / 
var , nous ne pouvons pas avoir de compteur pour incrémenter le délai pour chaque appel récursif; à la place, vous pouvez utiliser les arguments d'une fonction récursive pour effectuer un incrément lors d'un appel récursif.
Voici comment résoudre ce problème:
 const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1); 
Tâche # 4Écrivez un script qui affichera le message «Hello World» avec la même structure de retard que dans la tâche # 3, mais cette fois en groupes de 5 messages, et le groupe aura un intervalle de retard principal. Pour le premier groupe de 5 messages, nous sélectionnons le retard initial de 100 ms, pour les prochains - 200 ms, pour le troisième - 300 ms et ainsi de suite.
Voici comment ce script devrait fonctionner:
- À 100 ms, le script affiche «Hello World» pour la première fois, et le fait 5 fois avec un intervalle augmentant de 100 ms. Le premier message apparaîtra après 100 ms, le second après 200 ms, etc.
- Après les 5 premiers messages, le script devrait augmenter le retard principal de 200 ms. Ainsi, le 6ème message sera affiché après 500 ms + 200 ms (700 ms), le 7ème - 900 ms, le 8ème message - après 1100 ms, etc.
- Après 10 messages, le script devrait augmenter l'intervalle de retard principal de 300 ms. Le 11ème message devrait s'afficher après 500 ms + 1000 ms + 300 ms (18000 ms). Le 12ème message devrait s'afficher après 2100 ms, etc.
Selon ce principe, le programme devrait fonctionner indéfiniment.
Incluez un retard dans le message affiché. Vous devriez obtenir quelque chose comme ça (aucun commentaire):
Hello World. 100 // 100
Hello World. 100 // 200
Hello World. 100 // 300
Hello World. 100 // 400
Hello World. 100 // 500
Hello World. 200 // 700
Hello World. 200 // 900
Hello World. 200 // 1100
...Limitations : vous ne pouvez utiliser que des appels à 
setInterval (et non 
setTimeout ) et une seule 
if .
SolutionPuisque nous ne pouvons travailler qu'avec des appels 
setInterval , nous devons ici utiliser la récursivité et également augmenter le délai du prochain appel 
setInterval . De plus, nous avons besoin de l' 
if pour que cela ne se produise qu'après 5 appels à cette fonction récursive.
Voici une solution possible:
 let lastIntervalId, counter = 5; const greeting = delay => { if (counter === 5) { clearInterval(lastIntervalId); lastIntervalId = setInterval(() => { console.log('Hello World. ', delay); greeting(delay + 100); }, delay); counter = 0; } counter += 1; }; greeting(100); 
Merci à tous ceux qui l'ont lu.