Minuteries JavaScript: tout ce que vous devez savoir

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ée

Les 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:

 // example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 ); 

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 arguments

Si 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.

 // : func(arg1, arg2, arg3, ...) //  : setTimeout(func, delay, arg1, arg2, arg3, ...) 

Voici un exemple:

 // example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js'); 

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 ° 1

Ainsi, 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.

Limitation

Dans 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.

Solution

Voici comment je résoudrais ce problème:

 // solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8); 

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 fonction

Mais 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 :

 // example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 ); 

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 minuteurs

Puisqu'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:

 // example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId); 

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 garantie

Avez-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:

 // example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { //    } 

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.

Solution

Voici 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); } }; //     : obj.whoCallMe 

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 ?

 //       ?? setTimeout(obj.whoCalledMe, 0); 

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 .

Solution

Puisque 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.

Source: https://habr.com/ru/post/fr426709/


All Articles