setTimeout(fn, 0)
setTimeout(fn, 0) не виконує функцію негайно, як може здатися: він ставить колбек fn у чергу на виконання після поточного коду, тобто асинхронно, у наступному циклі event loop. Нульова затримка це не «виконати зараз», а «виконати якнайшвидше, але після всього синхронного коду».
Теорія
TL;DR
setTimeout(fn, 0)планує виклик, а не виконує його.- Колбек виконається після того, як звільниться call stack, тобто після всього поточного синхронного коду.
- Фактична затримка більша за нуль: специфікація дозволяє браузеру мінімум близько 4 мс для вкладених таймерів.
- Причина в тому, що JavaScript однопотоковий: асинхронні колбеки чекають у черзі.
- Використовують, щоб відкласти код, не блокувати UI і дати браузеру оновити DOM.
Швидкий приклад
setTimeout(() => console.log('timeout'), 0);
console.log('main');Вивід:
main
timeoutХоча затримка дорівнює нулю, JavaScript спочатку виконує весь поточний стек, тобто все синхронне, і лише потім бере колбеки з черги таймерів (callback queue).
Чому так
JavaScript однопотоковий: у нього один call stack і одна черга задач. Усі асинхронні операції (setTimeout, fetch, Promise) потрапляють у чергу подій і виконуються тільки після того, як стек викликів звільниться. Event loop постійно перевіряє: якщо стек порожній, він бере наступну задачу з черги і кладе її в стек.
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');Вивід:
A
C
BНуль насправді не нуль
Затримка в setTimeout це мінімальний час очікування, а не гарантований. Колбек не запуститься, доки стек зайнятий, тому важка синхронна операція легко відкладе його на сотні мілісекунд.
setTimeout(() => console.log('timeout'), 0);
const end = Date.now() + 500;
while (Date.now() < end) {
// busy loop blocks the stack for half a second
}
console.log('main'); // main first, timeout only after the loop is overДодатково браузери обмежують вкладені таймери: починаючи приблизно з п'ятого рівня вкладеності мінімальна затримка стає близько 4 мс. У неактивній вкладці інтервал може бути ще більшим.
Для чого використовують setTimeout(fn, 0)
- Дочекатися завершення поточних операцій, перш ніж читати результат.
- Не блокувати інтерфейс: розбити важке обчислення на частини, між якими браузер встигне відмалювати кадр.
- Запустити код «після всього іншого», наприклад після того, як спрацюють усі обробники події.
- Дати браузеру оновити DOM перед наступним кроком.
button.addEventListener('click', () => {
status.textContent = 'Loading...'; // the browser can paint this first
setTimeout(() => {
runHeavyCalculation(); // deferred, so the text is visible
}, 0);
});Для роботи з анімацією краще підходить requestAnimationFrame, а для відкладання до наступної мікрозадачі, queueMicrotask або Promise.resolve().then(fn).
Підсумкова таблиця
| Що робить | Ставить колбек у чергу на наступний цикл подій |
|---|---|
| Час затримки | Мінімум 0 мс, фактично трохи пізніше |
| Виконання | Після всього синхронного коду |
| Використовують для | Відкладеного коду, синхронізації, уникнення блокування UI |
Типові помилки
- Вважати, що
setTimeout(fn, 0)виконає функцію негайно і результат буде доступний на наступному рядку. - Писати
setTimeout(fn(), 0): дужки виконують функцію одразу і передають таймеруundefined. - Розраховувати на точну затримку:
0це мінімум, реальний час залежить від зайнятості стека і від браузера. - Плутати з мікрозадачами:
Promise.resolve().then(fn)виконається раніше заsetTimeout(fn, 0). - Використовувати
setTimeout(fn, 0)для анімації замістьrequestAnimationFrameі отримувати ривки. - Не зберігати ідентифікатор таймера, коли виклик треба мати змогу скасувати через
clearTimeout.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.