Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «У чому різниця між process.nextTick() та setImmediate()?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**process.nextTick() проти setImmediate()**: `nextTick` дренує мікротаск-чергу до того як цикл подій переходить до наступної фази. `setImmediate` чекає фазу check, після опитування I/O. Рекурсивний `nextTick` морить голодом I/O; `setImmediate` ні. ```js process.nextTick(() => console.log('nextTick')); // спрацьовує першим setImmediate(() => console.log('setImmediate')); // у фазі check // Вивід: // nextTick // setImmediate ``` **Головне:** використовуй `setImmediate` за замовчуванням; `nextTick` тільки коли треба виконати до будь-якого I/O в тій самій ітерації.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**process.nextTick()** ставить колбек у чергу, яка виконується одразу після того як поточний стек викликів спустошується, але до того як цикл подій переходить до наступної фази. **setImmediate()** ставить колбек у чергу перевірки (check phase), яка запускається після фази опитування I/O. ## Теорія ### TL;DR - Цикл подій у Node.js має фази: timers, poll (I/O), check, close. `nextTick` спрацьовує між будь-якими двома фазами; `setImmediate` тільки у фазі check. - `process.nextTick()` використовує мікротаск-чергу, яку V8 повністю дренує до того як libuv переходить до наступної фази. `setImmediate()` реєструє обробник `uv_check_t` у libuv. - Рекурсивний `nextTick` морить голодом I/O. Рекурсивний `setImmediate` ні. - Правило вибору: треба виконати до будь-якого I/O? `nextTick`. Треба поступитися I/O і таймерам? `setImmediate`. - Обидва спрацьовують після синхронного коду. Порядок між ними передбачуваний тільки всередині I/O-колбека. ### Швидкий приклад ```js setImmediate(() => console.log('I - setImmediate')); process.nextTick(() => console.log('N - nextTick')); Promise.resolve().then(() => console.log('P - Promise')); console.log('sync'); // Вивід (завжди): // sync // N - nextTick // P - Promise // I - setImmediate ``` `nextTick` спрацьовує першим, бо V8 дренує мікротаск-чергу до того як цикл подій рухається далі. `Promise.then()` у тій самій черзі, але після `nextTick`-колбеків. `setImmediate` чекає фазу check. ### Головна різниця Різниця між `nextTick` і `setImmediate` не просто у часі запуску, а в тому, де в рушії живе колбек. Колбеки `nextTick` потрапляють у спеціальну чергу, яку Node дренує синхронно всередині `InternalCallbackScope`, до того як `uv_run()` взагалі щось робить. `setImmediate` реєструє обробник `uv_check_t` у libuv, і той спрацьовує тільки коли цикл подій досягає фази check після опитування I/O. Саме тому рекурсивний `nextTick` може повністю заблокувати I/O, а `setImmediate` ні. ### Коли що використовувати - Виконати до будь-якого I/O в поточній ітерації: `process.nextTick()` (наприклад, emit події після конструктора, щоб слухачі встигли підключитися). - Поступитися I/O і таймерам: `setImmediate()` (наприклад, відкласти очищення після `res.json()` в Express). - Рекурсивний опит або retry-цикли: тільки `setImmediate`. Рекурсія `nextTick` глибиною 1k+ блокує таймери на 100мс і більше. - Узгодженість з `Promise.then()`: `nextTick` спрацьовує до Promise-колбеків у тому самому циклі дренування мікротасків. ### Пріоритет у циклі подій | Пріоритет | Механізм | Черга / фаза | |-----------|----------|--------------| | 1 (найвищий) | `process.nextTick()` | Мікротаск (до фаз) | | 2 | `Promise.then()` | Мікротаск (до фаз) | | 3 | `setTimeout(fn, 0)` | Фаза timers | | 4 | `setImmediate()` | Фаза check | ### Як Node обробляє їх зсередини Node.js використовує libuv для циклу подій із фазами у такому порядку: timers, poll (I/O), check, close. `setImmediate()` реєструє обробник `uv_check_t`, який libuv викликає у `uv__run_check()` після `uv__io_poll()`. `process.nextTick()` обходить libuv повністю: деструктор `InternalCallbackScope` у Node скидає nextTick-чергу через `MicrotaskQueue::PerformCheckpointInternal()` у V8 після кожного скрипту або колбека, але до того як `uv_run()` переходить до наступної фази. Тому в документації Node.js написано, що `nextTick` технічно не є частиною циклу подій. Особисте спостереження з продакшену: загортати `process.exit()` у `nextTick` після I/O-колбека здається нешкідливим, але це може обрізати інші незавершені I/O-операції. `setImmediate` в цьому паттерні безпечніший. ### Типові помилки **Помилка 1: nextTick для будь-якого асинхронного відкладення** ```js fs.readFile('file.txt', (err, data) => { process.nextTick(() => process.exit(0)); // виходить до завершення інших I/O }); ``` `nextTick` спрацьовує до того як відновлюється фаза poll, тому інші незавершені fs-колбеки ніколи не виконаються. Тут потрібен `setImmediate`. **Помилка 2: рекурсивний nextTick у логіці опитування** ```js function poll() { process.nextTick(poll); // ніколи не поступається циклу подій } poll(); ``` Це морить голодом весь I/O і таймери. При глибині 1M+ цикл зависає безповоротно. Правильно: `setImmediate(poll)`. **Помилка 3: очікування що вкладений setImmediate виконається до зовнішнього** ```js process.nextTick(() => { setImmediate(() => console.log('вкладений setImmediate')); }); setImmediate(() => console.log('зовнішній setImmediate')); // Вивід: // зовнішній setImmediate // вкладений setImmediate ``` `setImmediate`, поставлений у черзі всередині `nextTick`-колбека, пропускає поточну фазу check і виконується в наступному циклі. Це дивує тих, хто очікує зворотного порядку. **Помилка 4: голодування у рекурсивній обробці** ```js setImmediate(() => console.log('I - setImmediate')); // поставлено в чергу let depth = 0; function recurse() { if (++depth > 5) return console.log('Done'); process.nextTick(recurse); } recurse(); // Вивід: // Done // I - setImmediate (затримано усіма 6 nextTick-викликами) ``` Шість викликів `nextTick` вже помітно затримують `setImmediate`. При реальних масштабах (глибина 1k+) ти блокуєш таймери на 100мс і більше, що ламає код чутливий до таймаутів. ### Де зустрічається у реальних проектах - **Express route-обробники**: `nextTick` для звільнення з'єднання з базою після `res.json()`, до відновлення фази poll. - **Паттерн EventEmitter**: відкладання `this.emit('ready')` у конструкторі через `nextTick`, щоб слухачі встигли підключитися синхронно. - **Hapi auth-плагіни**: `setImmediate` після обробки запиту, щоб не блокувати мережеві колбеки. - **async_hooks**: `nextTick` виконується в поточному async-контексті без затримки фази, корисно для before/after-хуків інструментації. - **PM2 кластеризація**: `setImmediate` для відкладання міжпроцесних повідомлень, щоб I/O воркерів не голодував. ### Питання на співбесіді **Q:** Чи може `setImmediate` виконатись раніше за `process.nextTick()`? **A:** У Node.js >= 11 при виклику обох з верхнього рівня модуля ні. У старіших версіях або в REPL з активною фазою timers порядок міг бути непередбачуваним. У сучасному Node мікротаск-черга завжди дренується до будь-якої фази. **Q:** Як `nextTick` і `Promise.then()` співвідносяться між собою? **A:** Обидва у мікротаск-черзі, яка дренується до переходу між фазами. `nextTick`-колбеки дренуються першими, потім Promise-колбеки. Тому `process.nextTick(cb)` виконується до `Promise.resolve().then(cb)` якщо обидва поставлені в одній ітерації. **Q:** Що станеться якщо викликати `nextTick` всередині `setImmediate`-колбека? **A:** `nextTick`-колбек виконається одразу після завершення `setImmediate`-колбека, до того як фаза check перейде до наступного `setImmediate` у черзі. Він не чекає наступної ітерації циклу подій. **Q:** (Senior) Що в libuv обробляє `setImmediate`, а що у V8 обробляє `nextTick`? Опиши порядок виконання. **A:** `setImmediate` реєструє `uv_check_t`-дескриптор. libuv викликає їх у `uv__run_check()` всередині `uv_run()`, після `uv__io_poll()`. `nextTick` не потрапляє в libuv взагалі: деструктор `InternalCallbackScope` у Node скидає nextTick-чергу через `MicrotaskQueue::PerformCheckpointInternal()` у V8 після кожної C++-межі колбека. Тобто `nextTick` спрацьовує в проміжку між будь-якими двома libuv-колбеками, а не у іменованій фазі. **Q:** Як діагностувати голодування від `nextTick` у продакшені? **A:** Використай `clinic.js doctor` або запусти Node з `--trace-event-categories v8`. Clinic показує затримку циклу подій у часі. Якщо затримка циклу зростає при низькому CPU, рекурсивний `nextTick` є найпоширенішою причиною. Додай лічильник і після N ітерацій переключись на `setImmediate`. ## Приклади ### Базовий: порядок виконання поряд ```js console.log('1'); process.nextTick(() => console.log('2 - nextTick')); Promise.resolve().then(() => console.log('3 - Promise')); setImmediate(() => console.log('4 - setImmediate')); setTimeout(() => console.log('5 - setTimeout'), 0); console.log('6'); // Вивід (Node 18): // 1 // 6 // 2 - nextTick // 3 - Promise // 4 - setImmediate (або 5 перед 4 для setTimeout - порядок між ними поза I/O не гарантований) ``` `nextTick` і `Promise` дренують мікротаск-чергу синхронно до будь-якої фази циклу. `setImmediate` і `setTimeout(fn, 0)` обидва у фазах циклу, і їхній відносний порядок поза I/O-колбеком не гарантований. ### Середній: паттерн конструктора EventEmitter ```js const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); // nextTick відкладає emit до завершення конструктора, // даючи час підключити слухача через .on() до того як подія спрацює process.nextTick(() => { this.emit('ready'); }); } } const emitter = new MyEmitter(); emitter.on('ready', () => console.log('готово!')); // Вивід: готово! // Без nextTick 'ready' спрацює під час конструктора і слухач ще не підключений ``` Це один із найпоширеніших законних використань `nextTick` у бібліотечному коді. `setImmediate` теж спрацює, але `nextTick` гарантує що emit відбудеться до будь-якого I/O в тій самій ітерації. ### Старший рівень: демонстрація голодування ```js setImmediate(() => console.log('setImmediate - має виконатись незабаром')); let depth = 0; function recurse() { if (++depth > 5) return console.log(`Done at depth ${depth}`); process.nextTick(recurse); } recurse(); // Вивід: // Done at depth 6 // setImmediate - має виконатись незабаром <-- затримано усіма 6 nextTick-викликами // При depth 1_000_000 setImmediate і будь-які таймери // були б заблоковані на сотні мілісекунд. // Виправлення: замінити process.nextTick(recurse) на setImmediate(recurse) ``` `setImmediate`-колбек, поставлений у чергу до `recurse()`, не виконується поки вся `nextTick`-ланцюжок не завершиться. У реальному сценарії з логером або health-check, що використовує рекурсивний `nextTick`, саме так непомітно ламаються операції чутливі до таймаутів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.