setTimeout і setInterval в JavaScript
setTimeout планує виконання колбека один раз після вказаної затримки; setInterval викликає його повторно з кожним тиком інтервалу, поки ти його не зупиниш.
Теорія
TL;DR
setTimeoutспрацьовує один раз, як таймер для однієї піци.setIntervalдзвонить постійно, як будильник кожні 5 хвилин, поки ти його не вимкнеш.- Головна різниця: одноразовий проти повторюваного.
- Потрібно виконати щось один раз?
setTimeout. Потрібен цикл?setInterval, але для асинхронних задач краще ланцюжокsetTimeout. - Обидва повертають ID для
clearTimeoutабоclearInterval. - Затримка - це мінімум, не гарантія. Event loop може додати більше часу.
Швидкий приклад
// setTimeout: спрацьовує ОДИН РАЗ після затримки
const timeoutId = setTimeout(() => console.log("Спрацювало один раз"), 100);
// clearTimeout(timeoutId); // скасувати до спрацювання
// setInterval: спрацьовує ПОВТОРНО кожен інтервал
const intervalId = setInterval(() => console.log("Тік"), 1000);
setTimeout(() => clearInterval(intervalId), 5000); // зупинити після ~5 тиківsetTimeout додає одну задачу в чергу. setInterval додає нову задачу кожен інтервал, незалежно від того, чи завершилася попередня.
Ключова різниця
setInterval планує наступний виклик одразу, як тільки минає інтервал. Він не чекає, поки колбек завершиться. Якщо колбек виконується 1.5 секунди, а інтервал - 1 секунда, виклики починають накопичуватися в черзі. setTimeout спрацьовує один раз і зупиняється. Якщо потрібен повторюваний таймер, який чекає завершення колбека, роби ланцюжок setTimeout всередині самого себе.
Коли що використовувати
- Приховати спінер після затримки:
setTimeout. - Опитувати API кожні 5 секунд:
setIntervalдля простих випадків, або ланцюжокsetTimeoutколи колбек асинхронний. - Дебаунс поля пошуку: ланцюжок
setTimeoutзclearTimeoutна кожне натискання клавіші. - Плавна анімація 60 кадрів/с:
requestAnimationFrame, не таймери. - Уникнути блокування UI: будь-який таймер краще за цикл
while.
Таблиця порівняння
| Властивість | setTimeout | setInterval |
|---|---|---|
| Виконання | Один раз після затримки | Повторно кожен інтервал |
| Мінімальна затримка | ~4мс (браузери обмежують вкладені виклики) | ~4мс, але виклики можуть накладатись |
| Скасування | clearTimeout(id) | clearInterval(id) |
| Безпека з async | Передбачувано | Може накопичуватись, якщо колбек повільний |
| Коли використовувати | Дебаунс, одноразові затримки | Лічильники, просте опитування |
Як браузер це обробляє
setTimeout і setInterval - це не частина JavaScript-рушія. Вони живуть у шарі Web API браузера (або libuv у Node.js). Коли затримка закінчується, браузер кладе колбек у чергу макротасків. Event loop підхоплює його тільки після того, як стек викликів і всі мікротаски (Promise) очищені. Саме тому setTimeout(fn, 0) не запускається миттєво. Нуль мілісекунд - це не нуль часу.
Браузери також встановлюють мінімум ~4мс для вкладених викликів setTimeout після 5 рівнів вкладеності (специфікація HTML5). У фонових вкладках таймери можуть гальмуватися до 1000мс.
Типові помилки
Передача виклику функції замість посилання
function greet(name) { console.log(`Привіт ${name}`); }
setTimeout(greet("Аліса"), 1000); // виконується одразу, передає undefined
setTimeout(() => greet("Аліса"), 1000); // правильноgreet("Аліса") виконується негайно і повертає undefined. Ти передаєш цей результат у setTimeout, а не саму функцію.
Забутий cleanup у React компонентах
// Витік пам'яті: інтервал продовжує жити після демонтування
useEffect(() => {
setInterval(() => fetchData(), 5000);
}, []);
// Правильно
useEffect(() => {
const id = setInterval(fetchData, 5000);
return () => clearInterval(id);
}, []);Інтервал продовжує працювати після демонтування компонента. Виправлення - один рядок: повернути функцію cleanup.
setInterval з повільними асинхронними колбеками
// Виклики можуть накластись, якщо fetch займає більше 5с
setInterval(async () => {
const data = await fetch("/api/status");
updateUI(data);
}, 5000);
// Краще: чекати завершення кожного виклику
function poll() {
fetch("/api/status")
.then(res => res.json())
.then(data => {
updateUI(data);
setTimeout(poll, 5000);
});
}
poll();Спроба змінити інтервал зсередини колбека
let delay = 1000;
setInterval(() => {
delay *= 2; // нічого не змінює - інтервал зафіксований при плануванні
}, delay);
// Якщо потрібні динамічні затримки - використовуй ланцюжок setTimeout:
let delay = 1000;
function tick() {
console.log("Тік");
delay *= 2;
setTimeout(tick, delay);
}
setTimeout(tick, delay);Де зустрічається у продакшені
- React поле пошуку:
useEffect+setTimeoutдля дебаунсу 300мс,clearTimeoutу cleanup. - Redux DevTools:
setIntervalдля періодичних знімків стану. - VS Code: ланцюжки
setTimeoutдля дебаунсу автодоповнення при введенні. - Express/Node.js: таймери для коректного завершення сервера,
setTimeoutдля примусового закриття після вікна cleanup. - Для анімацій:
requestAnimationFrameзамістьsetInterval. Для реал-тайм даних: WebSocket замість опитування.
Питання на співбесіді
Q: Чому setTimeout(fn, 0) не виконується миттєво?
A: Колбек потрапляє в чергу макротасків. Event loop спочатку виконує весь синхронний код і мікротаски, і лише потім підхоплює його. Нульова затримка означає «якнайшвидше після поточної роботи», а не «прямо зараз».
Q: Що таке обмеження ~4мс?
A: Специфікація HTML5 встановлює мінімум 4мс для вкладених викликів setTimeout після 5 рівнів вкладеності. У фонових вкладках браузери можуть гальмувати таймери до 1000мс і більше для економії CPU.
Q: Коли варто використовувати ланцюжок setTimeout замість setInterval?
A: Коли колбек асинхронний або займає непередбачуваний час. Ланцюжок гарантує, що наступний виклик починається тільки після завершення попереднього, тому виклики не накладаються.
Q: Як безпечно скасувати setInterval, якщо ID перехоплений у замиканні (closure)?
A: Зберігай ID у змінній поза колбеком: const id = setInterval(...); setTimeout(() => clearInterval(id), 5000). Не покладайся на те, що колбек сам збереже свій ID.
Q: У вкладці, де задачі займають 100мс, що станеться з setInterval(..., 10)?
A: Колбеки накопичуються у черзі, але не виконуються паралельно - JS однопоточний. Event loop обробляє їх по одному, тому реальні інтервали будуть близько 100мс, а не 10мс. Результат - черга, а не паралелізм.
Приклади
Базовий: відкладена одноразова дія
console.log("Старт");
const id = setTimeout(() => console.log("Спрацювало"), 100);
console.log("Кінець");
// Вивід: Старт -> Кінець -> Спрацювало
// "Кінець" виводиться до "Спрацювало", бо setTimeout асинхроннийsetTimeout не блокує. Движок одразу переходить до console.log("Кінець") і виконує колбек пізніше з черги макротасків.
Середній: дебаунс (debounce) поля пошуку в React
function SearchInput() {
const [query, setQuery] = useState("");
useEffect(() => {
const id = setTimeout(() => {
if (query) fetchResults(query);
}, 300);
return () => clearTimeout(id); // скасувати, якщо користувач продовжує друкувати
}, [query]);
return <input onChange={e => setQuery(e.target.value)} />;
}
// Кожне натискання клавіші скидає таймер.
// API-запит відправляється тільки після 300мс тиші.Коли query змінюється, старий таймер скасовується і стартує новий. Запит іде тільки тоді, коли користувач зупинився.
Просунутий: ланцюжок setTimeout проти setInterval для опитування API
// Версія з setInterval - може накладатись, якщо fetch повільний
const id = setInterval(async () => {
const res = await fetch("/api/health");
const data = await res.json();
console.log(data.status);
}, 2000);
// Ланцюжок setTimeout - чекає кожної відповіді
function pollHealth() {
fetch("/api/health")
.then(r => r.json())
.then(data => {
console.log(data.status);
setTimeout(pollHealth, 2000); // наступний запит після завершення цього
})
.catch(() => setTimeout(pollHealth, 5000)); // відступ при помилці
}
pollHealth();Ланцюжковий варіант дозволяє адаптувати затримку за результатом і чисто обробляти помилки. Варіант з setInterval може спричинити подвійні запити у продакшені, коли сервери починають гальмувати під навантаженням: один виклик ще не завершився, а наступний вже стартував.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.