Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працюють таймери та планування в Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Таймери в Node.js** - функції для планування колбеків у конкретних фазах циклу подій (event loop). ```js process.nextTick(() => console.log('1')); // мікрозавдання, перший Promise.resolve().then(() => console.log('2')); // мікрозавдання, другий setTimeout(() => console.log('3'), 0); // фаза таймерів setImmediate(() => console.log('4')); // фаза перевірки ``` **Ключове:** мікрозавдання (`nextTick`, Promise) завжди виконуються до будь-яких таймерних колбеків.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Таймери в Node.js** - функції для планування коду у прив'язці до конкретних фаз циклу подій (event loop), а не до точного астрономічного часу. ## Теорія ### TL;DR - Чотири функції планування: `setTimeout`, `setInterval`, `setImmediate`, `process.nextTick` - `process.nextTick` і виконані Promise є мікрозавданнями - вони запускаються до будь-яких I/O колбеків - `setImmediate` спрацьовує у фазі перевірки, після I/O poll; `setTimeout(fn, 0)` - у фазі таймерів - Таймери гарантують мінімальну затримку, а не точний час - Поза I/O колбеком порядок `setTimeout(fn, 0)` і `setImmediate` є недетермінованим ### Швидкий приклад ```js console.log('start'); process.nextTick(() => console.log('nextTick')); // мікрозавдання, перший Promise.resolve().then(() => console.log('Promise')); // мікрозавдання, другий setTimeout(() => console.log('setTimeout'), 0); // фаза таймерів setImmediate(() => console.log('setImmediate')); // фаза перевірки console.log('end'); // Вивід: // start // end // nextTick // Promise // setTimeout (може помінятися з setImmediate поза I/O) // setImmediate ``` Черга мікрозавдань очищується повністю до того, як цикл подій переходить до наступної фази. Це правило не змінюється. ### Як цикл подій планує кожен таймер Node.js виконує цикл подій за впорядкованими фазами. Кожна функція таймера підключається до своєї. `process.nextTick` технічно не є частиною циклу подій. Node очищає чергу nextTick після кожної окремої операції ще до того, як передає керування libuv. Тому рекурсивні виклики nextTick повністю блокують I/O. `Promise.then` колбеки потрапляють у чергу мікрозавдань і виконуються одразу після nextTick. З Node.js 11+ мікрозавдання очищаються між кожним окремим колбеком циклу, а не тільки між фазами. `setTimeout(fn, delay)` реєструє колбек у фазі таймерів. З `delay = 0` реальний мінімум становить приблизно 1мс. Точний момент спрацювання залежить від того, скільки часу займає поточна фаза і якою є роздільна здатність таймера ОС. `setImmediate` спрацьовує у фазі перевірки, одразу після завершення poll-фази. Всередині I/O колбека `setImmediate` завжди спрацьовує раніше `setTimeout(fn, 0)`. Поза ним покладатися на порядок не варто. `setInterval` поводиться як повторні виклики `setTimeout`, але не враховує час виконання колбека. Під навантаженням інтервали зміщуються, бо наступний таймер відраховується від моменту планування, а не від завершення попереднього колбека. ### Точність таймерів Таймери позначають мінімальний поріг, а не відлік годинника: ```js const start = Date.now(); setTimeout(() => { console.log(`Затримка: ${Date.now() - start}мс`); // Запит: 100мс. Факт: 101-115мс, іноді більше під навантаженням }, 100); ``` Якщо цикл подій зайнятий синхронним завданням, таймер спрацьовує пізніше. Переривати виконуваний синхронний блок неможливо. Це зазвичай дивує команди, які планують важливі задачі через `setTimeout` і виявляють дрейф лише після першого навантаження. ### Промісовані таймери (Node.js 15+) `timers/promises` дає awaitable-версії всіх трьох функцій: ```js const { setTimeout: sleep, setImmediate: immediate } = require('timers/promises'); async function example() { await sleep(1000); // чекає 1 секунду await immediate(); // передає керування фазі перевірки console.log('done'); } ``` Вони також приймають `AbortSignal`, що робить скасування зрозумілим: ```js const { setTimeout: sleep } = require('timers/promises'); const ac = new AbortController(); setTimeout(() => ac.abort(), 500); await sleep(2000, undefined, { signal: ac.signal }); // AbortError через 500мс ``` Не потрібно відстежувати ID таймерів вручну або розкидати `clearTimeout` по блоках try/catch. ### Типові помилки **1. Розрахунок на точний час у виробничих задачах** ```js // Ненадійно setTimeout(() => sendDailyReport(), 24 * 60 * 60 * 1000); ``` Процес може перезапуститися, цикл подій може бути заблокований, системний годинник може зміститися. Для важливих планових задач використовуй `node-cron` або чергу повідомлень. **2. Блокування I/O рекурсивним nextTick** ```js function loop() { process.nextTick(loop); // I/O ніколи не пройде } loop(); ``` Черга nextTick очищується повністю до I/O. Мережеві операції та читання файлів ніколи не виконаються. Для простого відкладення використовуй `setImmediate`. **3. Дрейф setInterval під навантаженням** ```js // Колбек виконується 200мс, інтервал 1000мс // Реальний проміжок: 800мс між кінцем колбека і наступним стартом setInterval(async () => { await processQueue(); // виконується змінний час }, 1000); ``` Коли час виконання колбека важливий, замість `setInterval` використовуй рекурсивний `setTimeout`. Запускай наступний таймер після завершення поточного колбека. **4. Припущення що setTimeout(0) завжди перший** ```js setTimeout(() => console.log('A'), 0); setImmediate(() => console.log('B')); // Вивід: A B або B A - залежить від роздільної здатності таймера ОС ``` Всередині I/O колбека завжди спочатку B, потім A. Поза ним порядок не гарантований специфікацією. ### Де зустрічається в реальному коді - **Debounce:** `clearTimeout` + `setTimeout` при кожному виклику, для пошукових полів і обробників resize - **Exponential backoff:** рекурсивний `setTimeout` з затримкою `Math.pow(2, attempt) * 1000` між повторами запитів - **Розбиття CPU-роботи:** `setImmediate` між ітераціями для передачі керування I/O без блокування циклу - **Таймаут запиту:** `AbortController` + `setTimeout` + `clearTimeout` після отримання відповіді - **Graceful shutdown:** `setTimeout(process.exit, 5000)` як примусова зупинка після SIGTERM ### Питання на співбесіді **Q:** Яка різниця між `process.nextTick` і `setImmediate`? **A:** `process.nextTick` запускається до переходу циклу подій до будь-якої наступної фази, повністю очищаючи свою чергу. `setImmediate` спрацьовує у фазі перевірки, після I/O poll. Назви вводять в оману: `setImmediate` насправді означає "на наступній ітерації циклу". **Q:** Чому порядок `setTimeout(fn, 0)` і `setImmediate` є недетермінованим поза I/O? **A:** Тому що `setTimeout` з delay 0 встановлює мінімум близько 1мс. Якщо ця 1мс вже минула до перевірки фази таймерів, `setTimeout` спрацьовує першим. Якщо ні, цикл переходить до фази перевірки і першим спрацьовує `setImmediate`. Все залежить від роздільної здатності таймера ОС у цей момент. **Q:** Чи може `process.nextTick` заблокувати сервер? **A:** Переповнення стека не буде, але I/O заблокується. Якщо продовжувати додавати nextTick колбеки зсередини nextTick, мережеві події та читання файлів ніколи не будуть оброблені. `setImmediate` безпечніший для простого відкладення виконання. **Q:** Як Node.js поводиться між таймерами, коли більше нічого не відбувається? **A:** libuv обчислює час до найближчого таймера і блокує poll-фазу рівно на цей час на рівні ОС. Тобто `setTimeout(fn, 1000)` без іншої роботи не навантажує CPU. Node спить до спрацювання таймера. **Q:** Коли варто використовувати `timers/promises` замість callback API? **A:** Коли ти всередині async-функції і потрібне чисте скасування через AbortSignal. Callback API не підтримує AbortSignal і змушує відстежувати ID таймерів вручну по всьому коду. ## Приклади ### Порядок виконання всіх чотирьох функцій ```js // Запусти в Node.js щоб перевірити свою ментальну модель setImmediate(() => console.log('setImmediate')); setTimeout(() => console.log('setTimeout(0)'), 0); process.nextTick(() => console.log('nextTick')); Promise.resolve().then(() => console.log('Promise.then')); // Гарантовано: // 1. nextTick // 2. Promise.then // 3. setTimeout(0) або setImmediate (недетерміновано поза I/O) // 4. setImmediate або setTimeout(0) ``` nextTick і Promise.then завжди першими. Між setTimeout(0) і setImmediate покладатися на порядок не варто. ### Повтор запиту з експоненційною затримкою ```js async function fetchWithRetry(url, maxAttempts = 4) { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { if (attempt === maxAttempts - 1) throw err; const delay = Math.pow(2, attempt) * 1000; // 1с, 2с, 4с await new Promise(resolve => setTimeout(resolve, delay)); } } } ``` Кожна невдала спроба чекає 1с, потім 2с, потім 4с перед наступною. Остання помилка пробрасується вище без змін. ### Розбиття CPU-роботи через setImmediate ```js function processLargeArray(items, callback) { let index = 0; function processChunk() { const end = Math.min(index + 100, items.length); while (index < end) { callback(items[index++]); } if (index < items.length) { setImmediate(processChunk); // передаємо керування I/O між чанками } } processChunk(); } ``` Без `setImmediate` обробка 100 000 елементів блокує цикл подій повністю на весь цей час. З ним HTTP-запити можуть оброблятися між чанками. Загальний час обробки трохи зростає, але сервер залишається чуйним.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.