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.