JavaScript setTimeout e setInterval
Impara a pianificare il codice in JavaScript con setTimeout e setInterval: sintassi, argomenti, annullamento timer, pattern ricorsivo, zero-delay, debouncing e throttling.
A volte non si vuole eseguire del codice subito — lo si vuole eseguire dopo, oppure ripetutamente. Le due funzioni di pianificazione di JavaScript, setTimeout() e setInterval(), permettono di fare esattamente questo. Questa guida ne illustra la sintassi, come passare argomenti, come annullare un timer pianificato, il fondamentale pattern del "setTimeout ricorsivo", il comportamento sorprendente di un ritardo pari a zero e due utilizzi pratici: il debouncing e il throttling.
Nessuna delle due funzioni fa parte del linguaggio JavaScript principale — sono fornite dall'ambiente host (browser e Node.js). Il comportamento descritto qui è lo stesso in entrambi, con alcune differenze indicate lungo il percorso.
Introduzione alle funzioni di temporizzazione in JavaScript
Un timer pianifica l'esecuzione di una callback dopo che il codice corrente ha terminato e trascorso un determinato intervallo di tempo. Poiché JavaScript è single-threaded, la callback non interrompe mai il codice in esecuzione; attende in una coda e viene eseguita solo quando lo stack di chiamate è vuoto. (Per il meccanismo completo, vedere The Event Loop.)
La funzione setTimeout()
setTimeout() esegue una funzione una volta dopo un ritardo specificato. Accetta una funzione da eseguire e un ritardo in millisecondi prima di quell'esecuzione.
Sintassi
let timerId = setTimeout(func, delay, arg1, arg2, ...);func— la funzione (o, meno comunemente, una stringa di codice) da eseguire.delay— l'attesa in millisecondi prima dell'esecuzione. Il valore predefinito è0.arg1, arg2, ...— argomenti opzionali passati direttamente afunc.
Il valore restituito è un id numerico del timer che è possibile passare in seguito a clearTimeout().
Esempio
Passare argomenti alla callback
Tutto ciò che si inserisce dopo il ritardo viene inoltrato alla callback. Questo è più pulito che racchiudere la chiamata in un'altra funzione freccia:
setTimeout(greet(), 1000) esegue greet() immediatamente e pianifica il suo valore di ritorno (probabilmente undefined). Scrivere setTimeout(greet, 1000) — senza parentesi.La funzione setInterval()
setInterval() ha la stessa firma di setTimeout(), ma invece di eseguire la callback una sola volta, la esegue ripetutamente ogni delay millisecondi finché non viene interrotta.
Sintassi
let timerId = setInterval(func, delay, arg1, arg2, ...);Esempio
setTimeout ricorsivo descritto di seguito.setTimeout ricorsivo vs. setInterval
È possibile riprodurre setInterval() facendo sì che la callback di un setTimeout() ripianifichi se stessa. La differenza fondamentale: setInterval() misura il ritardo tra gli avvii, mentre setTimeout() ricorsivo lo misura tra la fine di un'esecuzione e l'inizio della successiva — garantendo una pausa fissa anche quando la callback è lenta.
Annullamento dell'esecuzione pianificata
Entrambe le funzioni restituiscono un id del timer. Conservando quell'id è possibile annullare il lavoro pianificato con clearTimeout() o clearInterval(). (Le due funzioni di cancellazione sono in realtà intercambiabili nella maggior parte dei motori, ma abbinarle allo scheduler che ha creato l'id rende il codice più leggibile.)
Interrompere setTimeout()
Per annullare un timeout, memorizzare l'id restituito da setTimeout() e passarlo a clearTimeout() prima che il ritardo sia trascorso.
Esempio
Interrompere setInterval()
Analogamente, salvare l'id di setInterval() e passarlo a clearInterval(). Senza questo, l'intervallo gira all'infinito (o fino alla chiusura della pagina), il che è una fonte comune di memory leak e timer fuori controllo.
Esempio
Il setTimeout con ritardo zero
setTimeout(func, 0) non esegue func immediatamente. Pianifica l'esecuzione di func non appena il codice sincrono corrente ha terminato. Questo è un modo pratico per "cedere il controllo" — per consentire al browser di ridisegnare o per suddividere un'attività lunga in pezzi — e spiega l'ordine di output che spesso sorprende i principianti:
Si noti che i timer sono macrotask: vengono eseguiti dopo tutti i microtask in coda (come le callback delle promise risolte). Vedere Event loop: microtask e macrotask per le regole di ordinamento precise.
Applicazioni pratiche e consigli
Due degli utilizzi pratici più comuni di setTimeout() sono il debouncing e il throttling — entrambi sono modi per limitare la frequenza con cui una funzione viene eseguita in risposta a eventi rapidi e ripetuti.
Debouncing con setTimeout()
Il debouncing attende che una serie di eventi si sia arrestata prima di eseguire la funzione. Ogni nuovo evento reimposta il timer, quindi la callback si attiva solo dopo che le cose si sono calmate per wait millisecondi. Questo è ideale per una casella di ricerca: si vuole inviare una richiesta dopo che l'utente ha smesso di digitare, non una per ogni tasto premuto.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Debounced Input Example</title>
<script>
// Debounce function to limit the rate at which a function is executed
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Function to be debounced
function fetchData(input) {
alert(`API call with input: ${input}`); // Placeholder for an API call
}
// Create a debounced version of fetchData
const debouncedFetchData = debounce(fetchData, 300);
// Add the debounced function to an event listener
function setup() {
document.getElementById('searchInput').addEventListener('input', (event) => {
debouncedFetchData(event.target.value);
});
}
// Ensure setup is called once the document is fully loaded
document.addEventListener('DOMContentLoaded', setup);
</script>
</head>
<body>
<h3>Type in the input field:</h3>
<input type="text" id="searchInput" placeholder="Start typing..." />
</body>
</html>Throttling con setTimeout()
Il throttling esegue la funzione al massimo una volta ogni limit millisecondi, indipendentemente da quanti eventi arrivano nel frattempo. Dove il debouncing attende il silenzio, il throttling garantisce un ritmo costante — perfetto per i gestori di scroll, resize o mousemove che altrimenti si attiverebbero decine di volte al secondo. L'esempio seguente utilizza un approccio leading-edge (viene eseguito immediatamente al primo evento, poi impone il gap):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Throttled Scroll Event</title>
<style>
/* Simple styling for demonstration */
body, html {
height: 200%; /* Make the page scrollable */
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#log {
position: fixed;
top: 0;
left: 0;
background: white;
border: 1px solid #ccc;
padding: 10px;
width: 300px;
}
</style>
</head>
<body>
<div id="log">Scroll to see the effect...</div>
<script>
// Throttle function using setTimeout
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, Math.max(0, limit - (Date.now() - lastRan)));
}
}
}
// Function to be throttled
function handleScroll() {
const log = document.getElementById('log');
log.textContent = `Scroll event triggered at: ${new Date().toLocaleTimeString()}`;
}
// Add event listener for scroll
window.addEventListener('scroll', throttle(handleScroll, 1000));
</script>
</body>
</html>Aspetti da tenere a mente
- I ritardi sono un minimo, non una garanzia. Se lo stack di chiamate è occupato o l'event loop è congestionato, la callback attende. Il numero passato è il momento più presto in cui può essere eseguita, non una promessa.
- Le schede in background vengono rallentate. La maggior parte dei browser limita i timer nelle schede inattive a circa una volta al secondo per risparmiare energia, quindi le animazioni e il polling rallentano quando la scheda è nascosta.
- I timeout annidati vengono limitati a circa 4 ms. Dopo cinque chiamate
setTimeout()annidate, i browser impongono un ritardo minimo di circa 4 millisecondi, quindi un ritardo0non è mai veramente zero in catene profonde. - Ritardo massimo. Un ritardo superiore a
2147483647(2^31 − 1) va in overflow nel campo a 32 bit e viene trattato come0, scattando quasi immediatamente invece che in un futuro lontano. - Binding di
this. Quando si passa un metodo comesetTimeout(obj.method, 1000), questo perde il suothis. Usare una funzione freccia —setTimeout(() => obj.method(), 1000)— oppureobj.method.bind(obj). - Pulire sempre. Eliminare gli intervalli (e i timeout pendenti) quando un componente viene smontato o il lavoro non è più necessario, altrimenti si creeranno memory leak e si potrebbe operare su uno stato obsoleto.
Argomenti correlati
- Introduzione: callback — il fondamento su cui sono costruiti i timer.
- The Event Loop: microtask e macrotask — perché un timeout da
0ms viene comunque eseguito per ultimo. - Promise e Async/await — alternative moderne per sequenziare il lavoro asincrono.
- Ricorsione e stack — background per il pattern
setTimeoutricorsivo.
Conclusione
setTimeout() esegue il codice una volta dopo un ritardo; setInterval() lo esegue secondo una pianificazione ripetuta; clearTimeout() e clearInterval() li annullano. Ricordare che i ritardi sono minimi, passare gli argomenti dopo il ritardo anziché all'interno di un wrapper, ricorrere al pattern setTimeout ricorsivo quando si ha bisogno di una spaziatura costante e pulire sempre i timer non più necessari. Con il debouncing e il throttling in aggiunta, queste due piccole funzioni coprono la maggior parte del lavoro basato sul tempo che si esegue nel browser.