Peut-être que nous ne voulons pas exécuter une fonction tout de suite, mais à un certain moment dans le futur. Cela sâappelle âordonnancer (ou planifier) un appel de fonctionâ.
Il existe deux méthodes pour cela :
setTimeoutpermet dâexécuter une fonction une unique fois après un certain laps de temps.setIntervalnous permet dâexécuter une fonction de manière répétée, en commençant après lâintervalle de temps, puis en répétant continuellement à cet intervalle.
Ces méthodes ne font pas partie de la spécification JavaScript. Mais la plupart des environnements ont un planificateur interne et fournissent ces méthodes. En particulier, elles sont supportées par tous les navigateurs et Node.js.
setTimeout
La syntaxe :
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
Les paramètres :
func|code- Fonction ou chaîne de caractères représentant du code à exécuter. En général, câest une fonction. Pour des raisons historiques, une chaîne de caractères représentant du code peut être donnée en argument, mais ce nâest pas recommandé.
delay- La durée dâattente avant lâexécution, en millisecondes (1000ms = 1 seconde), par défaut 0.
arg1,arg2â¦- Arguments pour la fonction
Par exemple, le code ci-dessous appelle la fonction sayHi() une unique fois au bout de 1 seconde :
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
Dans le cas où fonction sayHi() requiert des arguments :
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Bonjour", "Jean"); // Bonjour, Jean
Si le premier argument est une chaîne de caractères, JavaScript crée alors une fonction à partir de celle-ci.
Ce qui fait que le code ci-dessous fonctionne aussi :
setTimeout("alert('Bonjour')", 1000);
Cependant, utiliser des chaînes de caractères nâest pas recommandé, il est préférable dâutiliser des fonctions fléchées à la place, comme ceci :
setTimeout(() => alert('Bonjour'), 1000);
Les développeurs novices font parfois lâerreur dâajouter des parenthèses () après la fonction :
// Faux!
setTimeout(sayHi(), 1000);
Cela ne fonctionne pas car setTimeout attend une référence à une fonciton. Ici sayHi() appelle la fonction et le résultat de cette exécution est passé à setTimeout. Dans notre cas, le résultat de sayHi() est undefined (la fonction ne renvoie rien), du coup, rien nâest planifié.
Annuler une tâche avec clearTimeout
Un appel à setTimeout renvoie un âidentifiant de timerâ timerId que lâon peut utiliser pour annuler lâexécution de la fonction.
La syntaxe pour annuler une tâche planifiée est la suivante :
let timerId = setTimeout(...);
clearTimeout(timerId);
Dans le code ci-dessous, nous planifions lâappel à la fonction avant de lâannuler, au final rien ne sâest passé :
let timerId = setTimeout(() => alert("Je n'arriverai jamais"), 1000);
alert(timerId); // Identifiant du timer
clearTimeout(timerId);
alert(timerId); // Le même identifiant (ne devient pas null après l'annulation)
Comme on peut le voir dans les résultats des alert, dans notre navigateur, lâidentifiant du timer est un nombre. Selon lâenvironnement, il peut être dâun autre type. Par exemple, Node.js renvoie un objet timer équipé dâautres méthodes.
Encore une fois, il nây a pas de spécification universelle pour ces méthodes, donc ce nâest pas gênant.
Pour les navigateurs, les timers sont décrits dans la section des timers de HTML Living Standard.
setInterval
La méthode setInterval a la même syntaxe que setTimeout:
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
Tous ses arguments ont la même signfication que précédemment, mais contrairement à setTimeout, setInterval appelle la fonction non pas une fois, mais périodiquement après un interval de temps donné.
Afin dâannuler les appels futurs à la fonction, il est nécessaire dâappeler clearInterval(timerId).
Lâexemple suivant affiche le message toutes les 2 secondes, puis arrête la tâche au bout de 5 secondes :
// Se répète toutes les 2 secondes
let timerId = setInterval(() => alert('tick'), 2000);
// S'arrête après 5 secondes
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
alert est affichéDans la majorité des navigateurs, dont Chrome et Firefox, le timer interne continue à sâincrémenter pendant quâun message est affiché (via alert, confirm ou prompt).
Donc, si vous exécutez le code ci-dessus et que vous ne fermez pas la fenêtre alert pendant un certain temps, la prochaine alert sera affichée immédiatement lorsque vous le faites. Lâintervalle réel entre les alertes sera inférieur à 2 secondes.
setTimeout imbriqué
Il y a deux façon dâordonnancer lâexécution périodique dâune tâche.
Lâun est setInterval. Lâautre est un setTimeout imbriqué, comme ceci :
/** Au lieu de :
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
Le setTimeout ci-dessus planifie le prochain appel de la fonction à la fin de lâappel en cours (*).
Le setTimeout imbriqué est une méthode plus flexible que setInterval. Ainsi, le prochain appel peut être programmé différemment, en fonction des résultats de lâappel en cours.
Par exemple, on peut avoir besoin dâimplémenter un service qui envoie une requête à un serveur toutes les 5 secondes pour récupérer de la donnée, mais dans le cas où le serveur est surchargé, on doit augmenter le délai à 10 secondes, puis 20 secondes, 40 secondesâ¦
Voici le pseudo-code correspondant :
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// Augmente l'intervalle avant le prochain appel
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
Ou par exemple, si les fonction quâon souhaite planifier demandent beaucoup de ressources CPU, on peut alors mesurer leur temps dâexécution et planifier le prochain appel en fonction.
Et si les fonctions que nous planifions sont gourmandes en ressources processeur, nous pouvons mesurer le temps pris par lâexécution et planifier le prochain appel tôt ou tard.
Un setTimeout imbriqué permet de définir le délai entre les exécutions plus précisément que setInterval.
Comparons deux blocs de codes, le premier utilise setInterval :
let i = 1;
setInterval(function() {
func(i++);
}, 100);
Le second utilise un setTimeout imbriqué :
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
Dans le cas du setInterval lâordonnanceur interne va appeler func(i++) toutes les 100ms :
Rien dâétrange ?
Le vrai délai entre deux appels à func est plus court que dans le code.
Câest normal car le temps dâexécution de func âconsommeâ une partie de ce délai.
Il est donc possible que le temps dâexécution de func soit plus long que prévu et prenne plus de 100ms.
Dans ce cas le moteur interne attend que lâexécution de func soit terminée, puis consulte lâordonnanceur et si le délai est déjà âconsomméâ, il réexécute la fonction immédiatement.
Dans ce cas extrême, si la fonction qui sâexécute met toujours plus de temps que delay ms, alors les appels successifs vont sâeffectuer sans aucun temps de pause.
Et voici lâimage pour le setTimeout imbriqué :
Le setTimeout imbriqué garantit le délai fixé (ici 100 ms).
Dans ce cas, câest parce que le nouvel appel est planifié à la fin du précédent.
Quand une fonction est passée à setInterval/setTimeout, une référence interne à cette fonction est créée et conservée dans lâordonnanceur. Cela empêche que la fonction soit détruite par le ramasse-miettes, même si il nây a pas dâautres références à cette dernière.
// La fonction reste en mémoire jusqu'à ce que l'ordonnanceur l'exécute
setTimeout(function() {...}, 100);
Pour setInterval, la fonction reste en mémoire jusquâà ce quâon appelle clearInterval.
Mais il y a un effet de bord, une fonction référence lâenvironement lexical extérieur, donc tant quâelle existe, les variables extérieures existent aussi. Ces variables peuvent occuper autant dâespace mémoire que la fonction elle-même. De ce fait quand on nâa plus besoin dâune fonction planifiée, il est préférable de lâannuler, même si elle est courte.
setTimeout sans délai
Il y a un cas dâusage particulier : setTimeout(func, 0) ou plus simplement setTimeout(func).
Ceci programme lâexécution de func dès que possible. Mais le planificateur ne lâinvoquera quâune fois le script en cours dâexécution terminé.
La fonction est donc programmée pour sâexécuter âjuste aprèsâ le script en cours.
Par exemple, le code ci dessous affiche âHelloâ, et immédiatement après, âWorldâ :
setTimeout(() => alert("World"));
alert("Hello");
La première ligne âmet lâappel dans le calendrier après 0 msâ. Mais le planificateur âvérifiera le calendrierâ uniquement une fois le script en cours terminé. "Hello" est donc le premier, et "World" â après.
Il y a aussi dâautres cas dâusage avancés dâordonnancement à délai nul, spécifique au cas des navigateurs web, dont nous parlerons dans le chapitre La boucle d'événement: les microtâches et les macrotâches.
Dans le navigateur, la fréquence dâexécution des timers imbriqués est limitée. Le HTML Living Standard indique : âaprès cinq timers imbriqués, lâintervalle est forcé dâêtre dâau moins 4 millisecondes.â.
Nous allons illustrer ce que cela veut dire dans lâexemple ci-dessous. Lâappel à setTimeout sây ré-ordonnance lui-même avec un délai nul. Chaque appel se souvient de lâheure de lâappel précédent grâce au tableau times. Cela va nous permettre de mesurer les délais réels entre les exécutions :
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // on garde en mémoire le délai depuis l'appel précédent
if (start + 100 < Date.now()) alert(times); // on affiche les délais si plus de 100ms se sont écoulées
else setTimeout(run); // sinon on planifie un nouvel appel
});
// voici un exemple de résultat :
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Les 4 premiers timers sâexécutent immédiatemment (comme indiqué dans la spécification), ensuite on peut voir 9, 15, 20, 24.... Le délai minimum de 4ms entre appel entre alors en jeu.
Cette même limitation sâapplique si on utilise setInterval au lieu de setTimeout : setInterfal(f) appelle f un certain nombre de fois avec un délai nul avant dâobserver un délai dâau moins 4ms.
Cette limitation est lâhéritage dâun lointain passé et beaucoup de scripts se basent dessus, dâoù la nécessité de cette limitation pour des raisons historiques.
Pour le JavaScript côté serveur, cette limitation nâexiste pas, et il existe dâautres façon de planifier immédiatement des tâches asynchrones, notamment setImmediate pour Node.js. Il faut donc garder à lâesprit que ce nota bene est spécifique aux navigateurs web.
Résumé
- Les méthodes
setInterval(func, delay, ...args)etsetTimeout(func, delay, ...args)permettent dâexécuterfuncrespectivement une seul fois/périodiquement aprèsdelaymillisecondes. - Pour annuler lâexécution, nous devons appeler
clearInterval/clearTimeoutavec la valeur renvoyée parsetInterval/setTimeout. - Les appels de
setTimeoutimbriqués sont une alternative plus flexible ÃsetInterval, ils permettent de configurer le temps entre les exécution plus précisément. - Lâordonnancement à délai nul avec
setTimeout(func, 0)(le même quesetTimeout(func)) permet de planifier lâexécution âdès que possible, mais seulement une fois que le bloc de code courant a été exécutéâ. - Le navigateur limite le délai minimal pour cinq appels imbriqués ou plus de
setTimeoutou poursetInterval(après le 5ème appel) à 4 ms. Câest pour des raisons historiques.
Veuillez noter que toutes les méthodes de planification ne garantissent pas le délai exact.
Par exemple, le timer interne au navigateur peut être ralenti pour de nombreuses raisons :
- Le CPU est surchargé.
- Lâonglet du navigateur est en tâche de fond.
- Lâordinateur est en mode économie dâénergie.
Tout ceci peut augmenter la résolution de lâhorloge (le délai minimum) jusquâà 300ms voire 1000ms en fonction du navigateur et des paramètres de performance au niveau du système dâexploitation.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)