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âŠ)