Un compte à rebours, une horloge, un rafraîchissement de données toutes les trente secondes : ces besoins ont l'air identiques, et pourtant l'un d'eux tolère mal la solution évidente.
setInterval est simple à écrire et facile à mal utiliser. Comprendre ce qu'il garantit, et surtout ce qu'il ne garantit pas, évite la plupart des ennuis.
Définition
setInterval(callback, delai) demande à l'environnement de replanifier la même fonction toutes les delai millisecondes, indéfiniment, jusqu'à l'appel de clearInterval avec l'identifiant renvoyé.
let restant = 3;
const id = setInterval(() => {
console.log(restant);
restant--;
if (restant === 0) {
clearInterval(id);
console.log("terminé");
}
}, 1000);
// 3, 2, 1, terminéLa condition d'arrêt vit à l'intérieur du callback : sans elle, l'intervalle tourne tant que la page est ouverte, ou tant que le processus vit sous Node.js.
Le décalage qui s'installe
L'intervalle décompte à partir de la planification, pas de la fin du callback. Si le traitement dure plus longtemps que le délai, le rythme demandé devient impossible à tenir et l'écart se creuse à chaque tour.
const t0 = Date.now();
let tour = 0;
const id = setInterval(() => {
tour++;
const debut = Date.now();
while (Date.now() - debut < 30) {} // 30 ms de travail dans un intervalle de 10
console.log("tour", tour, "à", Date.now() - t0, "ms");
if (tour === 3) clearInterval(id);
}, 10);Les tours ne tombent jamais à 10, 20 et 30 millisecondes : chacun démarre après la fin du précédent. Une horloge fondée sur le nombre de tours dérive donc, là où une horloge qui relit Date.now reste juste.
La version qui ne se chevauche pas
Pour un travail de durée variable, un appel réseau par exemple, on replanifie soi-même avec setTimeout() après chaque exécution. Le délai devient une pause entre deux tours, ce qui interdit tout empilement.
async function rafraichirEnBoucle(pas) {
while (true) {
await lireDonnees(); // durée inconnue
await new Promise((r) => setTimeout(r, pas)); // pause après coup
}
}
// ou, sans async :
function boucle(pas) {
setTimeout(() => { faireLeTravail(); boucle(pas); }, pas);
}Un intervalle oublié est une fuite classique. Dans une interface à composants, tout setInterval créé à l'affichage doit être annulé au retrait, sans quoi il continue de tourner sur un écran qui n'existe plus et déclenche des mises à jour dans le vide.
Questions fréquentes
Les exécutions peuvent-elles se chevaucher ?
Pas sur le fil : la boucle d'événements n'exécute qu'une chose à la fois, donc deux tours ne tournent jamais ensemble. En revanche la Promise lancée par un tour, une requête par exemple, peut très bien être encore en cours quand le tour suivant démarre. C'est là que les données se mélangent.
Que se passe-t-il dans un onglet en arrière-plan ?
Les navigateurs espacent fortement les intervalles d'un onglet caché, souvent à une exécution par seconde au mieux. Un compte à rebours fondé sur le nombre de tours affichera donc une valeur fausse au retour : recalculez toujours depuis un horodatage de départ.
Faut-il préférer setTimeout récursif ?
Dès que la durée du traitement est incertaine, oui. setInterval reste parfaitement adapté aux traitements courts et prévisibles, comme le rafraîchissement d'un affichage. Pour tout ce qui appelle le réseau, la version récursive évite les tours qui se marchent dessus.