🌐 AIæœçŽą & 代理 䞻饔
19 octobre 2023

L'ordonnancement avec setTimeout et setInterval

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 :

  • setTimeout permet d’exĂ©cuter une fonction une unique fois aprĂšs un certain laps de temps.
  • setInterval nous 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);
Passer une fonction, mais sans l’exĂ©cuter

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);
Le temps continue de s’écouler pendant que 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.

Le ramasse-miettes et le callback setInterval/setTimeout

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.

Un dĂ©lai nul n’est pas vraiment nul (pour un navigateur)

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) et setTimeout(func, delay, ...args) permettent d’exĂ©cuter func respectivement une seul fois/pĂ©riodiquement aprĂšs delay millisecondes.
  • Pour annuler l’exĂ©cution, nous devons appeler clearInterval/clearTimeout avec la valeur renvoyĂ©e par setInterval/setTimeout.
  • Les appels de setTimeout imbriquĂ©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 que setTimeout(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 setTimeout ou pour setInterval (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.

Exercices

importance: 5

Écrire une fonction printNumbers(from, to) qui affiche un nombre par seconde, en partant de from jusqu’à to.

Faites deux variantes de la solution :

  1. utilisant setInterval,
  2. Utilisation de setTimeout imbriqué.

Avec setInterval:

function printNumbers(from, to) {
  let current = from;

  let timerId = setInterval(function() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }, 1000);
}

// usage:
printNumbers(5, 10);

Utilisation de setTimeout imbriqué :

function printNumbers(from, to) {
  let current = from;

  setTimeout(function go() {
    alert(current);
    if (current < to) {
      setTimeout(go, 1000);
    }
    current++;
  }, 1000);
}

// utilisation :
printNumbers(5, 10);

Notons que, dans les deux solutions, il y a un délai initial avant le premier résultat. En effet, la fonction est appelée pour la premiÚre fois au bout de 1000ms.

Afin d’exĂ©cuter la fonction immĂ©diatement, on peut ajouter un autre appel avant setInterval.

function printNumbers(from, to) {
  let current = from;

  function go() {
    alert(current);
    if (current == to) {
      clearInterval(timerId);
    }
    current++;
  }

  go();
  let timerId = setInterval(go, 1000);
}

printNumbers(5, 10);
importance: 4

Voici une fonction qui utilise un setTimeout imbriqué pour découper une tùche en petit bouts.

Réécrire le bloc suivant en utilisant setInterval:

let i = 0;

let start = Date.now();

function count() {

  if (i == 1000000000) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count);
  }

  // un morceau d'une trĂšs grosse tĂąche
  for(let j = 0; j < 1000000; j++) {
    i++;
  }

}

count();
importance: 5

Dans le code ci-dessous il y a une exécution planifié par setTimeout, suivie par un calcul conséquent qui prend plus de 100ms à tourner.

Quand la fonction planifiĂ©e va-t-elle s’exĂ©cuter ?

  1. AprĂšs la boucle.
  2. Avant la boucle.
  3. Au début de la boucle.

Qu’est-ce que alert va afficher ?

let i = 0;

setTimeout(() => alert(i), 100); // ?

// on considÚre que cette fonction met plus de 100ms à s'exécuter
for(let j = 0; j < 100000000; j++) {
  i++;
}

setTimeout ne peut s’exĂ©cuter qu’une fois le bloc de code courant terminĂ©.

Le i sera donc le dernier : 100000000.

let i = 0;

setTimeout(() => alert(i), 100); // 100000000

// on considÚre que cette fonction met plus de 100ms à s'exécuter
for(let j = 0; j < 100000000; j++) {
  i++;
}
Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)