Вкладені таймери
Багато «вкладених» setTimeout, тобто коли всередині колбека ставиться наступний таймер, не переповнюють стек викликів, але можуть забити чергу задач, спричинити дрейф часу, throttling і лаги інтерфейсу. Стек очищується між викликами, а от точність тайминга втрачається.
Теорія
TL;DR
- Stack overflow не буде: кожен колбек завершується, перш ніж виконається наступний.
- Натомість росте черга макрозадач, і інтерфейс може підлагувати.
- Час «тікання» дрейфує: затримки накопичуються від кроку до кроку.
- Навіть із затримкою
0діє мінімальний інтервал, близько 4 мс, а в неактивній вкладці до 1000 мс і більше. - Між таймерами повністю виконуються всі мікрозадачі, тож вони подовжують кожен крок.
- Рекурсивний
setTimeoutкращий заsetInterval, коли колбек іноді триває довше за інтервал.
Швидкий приклад
function tick() {
// робота...
setTimeout(tick, 0); // наступний тік у наступному оберті Event Loop
}
setTimeout(tick, 0);Стек не росте, бо tick завершується одразу після планування наступного виклику. Але «тіки» йтимуть з обмеженням мінімальної затримки і з дрейфом.
Що саме відбувається
- Немає stack overflow. Кожен
setTimeoutставить наступну задачу в чергу макрозадач і завершує поточну, тож стек очищується між викликами. - Черга макрозадач росте. Якщо всередині кожного колбека ставити кілька нових таймерів або робити важку роботу, задачі почнуть накопичуватися, а інтерфейс підлагувати.
- Дрейф, тобто накопичення затримки. Момент спрацювання зсувається: колбек виконається не раніше за вказану затримку і ще пізніше, якщо стек був зайнятий. У довгих ланцюжках зсув накопичується.
- Мінімальна затримка і throttling. Навіть за
0мс браузери застосовують мінімальний інтервал, зазвичай близько 4 мс, а в неактивній вкладці до 1000 мс і більше. Вкладені таймери з наднизькими затримками все одно «затискаються». - Мікрозадачі між таймерами. Після кожного колбека виконуються всі мікрозадачі (
Promise.then,queueMicrotask). Якщо всередині таймера створювати багато мікрозадач, вони додатково подовжать крок ланцюжка. - Порядок виконання. Таймери беруться з черги за часом готовності; якщо кілька готові одночасно, порядок може відрізнятися між рушіями, тож покладатися на конкретний порядок не варто.
- Пам'ять. Якщо колбеки замикають великі об'єкти, а ланцюжок довгий або нескінченний, споживання пам'яті може зростати: об'єкти довше лишаються досяжними.
Як робити краще
-
Компенсуйте дрейф у періодичних задачах:
javascriptconst start = performance.now(); const step = 1000; // 1 секунда let n = 0; function loop() { n++; // ...корисна робота... const nextAt = start + n * step; setTimeout(loop, Math.max(0, nextAt - performance.now())); } setTimeout(loop, step); -
Для анімацій використовуйте
requestAnimationFrame, він синхронізований з перемальовуванням і менше «дрейфує». -
Для «кожні N мс» без накладань обирайте рекурсивний
setTimeout, як вище, а неsetInterval: так ви уникнете накопичення викликів, якщо колбек іноді триває довше за інтервал. -
Важку роботу виносьте у Web Worker, щоб не блокувати інтерфейс.
Рекурсивний setTimeout проти setInterval
| Властивість | Рекурсивний setTimeout | setInterval |
|---|---|---|
| Наступний запуск планується | Після завершення поточного колбека | Незалежно від того, чи завершився колбек |
| Накопичення викликів | Неможливе | Можливе, якщо колбек повільніший за інтервал |
| Пауза між запусками | Гарантована | Не гарантована |
| Змінна затримка | Легко, значення можна перерахувати щоразу | Ні, інтервал фіксований |
| Зупинка | clearTimeout(id) для останнього таймера | clearInterval(id) |
Висновок
Вкладені setTimeout це безпечний спосіб «розірвати» синхронну рекурсію, але:
- точного тайминга не буде;
- за великого обсягу задач ви отримаєте лаги і дрейф;
- браузер застосує мінімальні затримки і throttling.
Типові помилки
- Чекати на переповнення стека. Це не рекурсія у звичайному сенсі: стек між тіками порожній, тому помилки
Maximum call stack size exceededне буде, і проблема виявиться пізніше, у вигляді лагів. - Ставити кілька таймерів у кожному колбеку. Ланцюжок розгалужується, кількість задач у черзі зростає лавиноподібно.
- Розраховувати на затримку
0. Вкладені таймери затискаються до приблизно 4 мс, тож цикл на 1000 кроків триватиме щонайменше кілька секунд. - Рахувати час як
крок * кількість тіків. Через дрейф реальний час буде більшим; беріть його зperformance.now(). - Не зупиняти ланцюжок. Без прапорця або збереженого ID таймер працює вічно, навіть коли екран, що його запустив, уже закрито.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.