Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Вкладені таймери». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Багато вкладених `setTimeout`, коли в колбеку ставиться наступний таймер, не переповнюють стек, але можуть забити чергу задач, спричинити дрейф часу, throttling і лаги інтерфейсу.** Стек не росте, бо кожен колбек завершується до того, як виконається наступний: нова задача просто потрапляє у чергу макрозадач. Натомість платою стає точність: браузер тримає мінімальний інтервал близько 4 мс (у неактивній вкладці до 1000 мс і більше), затримки накопичуються, а важка робота всередині колбеків підвішує рендеринг. ```javascript function tick() { // робота... setTimeout(tick, 0); // наступний тік у наступному оберті Event Loop } setTimeout(tick, 0); ``` **Ключове:** вкладені таймери безпечні для стека, але не дають точного тайминга; для рівного ритму компенсуйте дрейф, для анімацій беріть `requestAnimationFrame`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Багато «вкладених» `setTimeout`, тобто коли всередині колбека ставиться наступний таймер, не переповнюють стек викликів, але можуть забити чергу задач, спричинити дрейф часу, throttling і лаги інтерфейсу.** Стек очищується між викликами, а от точність тайминга втрачається. ## Теорія ### TL;DR - Stack overflow не буде: кожен колбек завершується, перш ніж виконається наступний. - Натомість росте черга макрозадач, і інтерфейс може підлагувати. - Час «тікання» дрейфує: затримки накопичуються від кроку до кроку. - Навіть із затримкою `0` діє мінімальний інтервал, близько 4 мс, а в неактивній вкладці до 1000 мс і більше. - Між таймерами повністю виконуються всі мікрозадачі, тож вони подовжують кожен крок. - Рекурсивний `setTimeout` кращий за `setInterval`, коли колбек іноді триває довше за інтервал. ### Швидкий приклад ```javascript function tick() { // робота... setTimeout(tick, 0); // наступний тік у наступному оберті Event Loop } setTimeout(tick, 0); ``` Стек не росте, бо `tick` завершується одразу після планування наступного виклику. Але «тіки» йтимуть **з обмеженням мінімальної затримки** і **з дрейфом**. ### Що саме відбувається 1. **Немає stack overflow.** Кожен `setTimeout` ставить *наступну* задачу в чергу макрозадач і завершує поточну, тож стек очищується між викликами. 2. **Черга макрозадач росте.** Якщо всередині кожного колбека ставити кілька нових таймерів або робити важку роботу, задачі почнуть **накопичуватися**, а інтерфейс підлагувати. 3. **Дрейф, тобто накопичення затримки.** Момент спрацювання зсувається: колбек виконається **не раніше** за вказану затримку і ще **пізніше**, якщо стек був зайнятий. У довгих ланцюжках зсув накопичується. 4. **Мінімальна затримка і throttling.** Навіть за `0` мс браузери застосовують **мінімальний інтервал**, зазвичай близько 4 мс, а в неактивній вкладці до 1000 мс і більше. Вкладені таймери з наднизькими затримками все одно «затискаються». 5. **Мікрозадачі між таймерами.** Після кожного колбека виконуються **всі мікрозадачі** (`Promise.then`, `queueMicrotask`). Якщо всередині таймера створювати багато мікрозадач, вони додатково подовжать крок ланцюжка. 6. **Порядок виконання.** Таймери беруться з черги за часом готовності; якщо кілька готові одночасно, порядок може відрізнятися між рушіями, тож покладатися на конкретний порядок **не варто**. 7. **Пам'ять.** Якщо колбеки замикають великі об'єкти, а ланцюжок довгий або нескінченний, споживання пам'яті може **зростати**: об'єкти довше лишаються досяжними. ### Як робити краще - **Компенсуйте дрейф** у періодичних задачах: ```javascript const 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 таймер працює вічно, навіть коли екран, що його запустив, уже закрито.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.