Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Event Loop: мікрозадачі vs макрозадачі». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Event Loop** (цикл подій) обробляє задачі в такому порядку: весь синхронний код, потім усі мікрозадачі (Promise, `queueMicrotask`), потім одна макрозадача (`setTimeout`, I/O), і знову по колу. ```javascript console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); // Вивід: 1, 4, 3, 2 ``` **Ключове:** мікрозадачі завжди виконуються перед наступною макрозадачею, включно з `setTimeout(..., 0)`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Event Loop** (цикл подій) - це планувальник задач в JavaScript: спочатку виконується весь синхронний код, потім повністю вичерпується черга мікрозадач (Promise, `queueMicrotask`), за потреби відбувається рендеринг, потім береться одна макрозадача (`setTimeout`, I/O), і так по колу. ## Теорія ### TL;DR - Аналогія: один кухар (call stack), термінові замовлення (мікрозадачі), звичайні замовлення (макрозадачі). Усі термінові виконуються перед тим як береться наступне звичайне. - Мікрозадачі виконуються пачкою після кожного синхронного блоку. Макрозадачі по одній, між ними браузер може малювати. - Мікрозадачі блокують рендеринг, якщо постійно ставлять одна одну в чергу. Одинарний `setTimeout` цього не робить. - Потрібен результат до наступного paint? Мікрозадача. Може почекати кадр? Макрозадача. ### Швидкий приклад ```javascript console.log('1. Sync'); setTimeout(() => console.log('2. Macrotask'), 0); Promise.resolve() .then(() => console.log('3. Microtask')); console.log('4. Sync'); // Вивід: // 1. Sync // 4. Sync // 3. Microtask // 2. Macrotask ``` Синхронний код виконується зверху вниз. Потім мікрозадачі. Потім макрозадачі - навіть якщо затримка в таймері 0ms. ### Головна різниця Черга мікрозадач завжди вичерпується повністю до того як відбудеться щось інше. Якщо мікрозадача ставить у чергу іншу мікрозадачу, та теж виконається в цьому ж циклі, до будь-якого рендеру чи макрозадачі. З макрозадачами інакше: браузер бере рівно одну за цикл, перевіряє чи треба малювати, і лише потім знову перевіряє мікрозадачі. Саме тому сотня ланцюжкових Promise може заморозити UI, а цикл `setTimeout(..., 0)` не може. ### Коли що використовувати - **Мікрозадачі:** оновлення стану, які мають відобразитись до наступного кадру (React batching, Vue реактивність), вимірювання DOM, валідація що йде в той самий рендер - **Макрозадачі:** таймери, I/O (`fetch`, читання файлів), обробники подій (click, scroll), все що може поступитися браузеру між кадрами Якщо робота має завершитись до наступного paint, використовуй мікрозадачу. Якщо може почекати кадр, підійде макрозадача. ### Таблиця порівняння | | Мікрозадачі | Макрозадачі | |---|---|---| | **Приклади** | `Promise.then`, `queueMicrotask()`, `MutationObserver` | `setTimeout`, `setInterval`, `fetch`, DOM events | | **Момент виконання** | Після поточного скрипту, до рендеру | По одній за цикл, після перевірки рендеру | | **Блокує рендеринг** | Так, якщо їх багато | Ні, браузер малює між ними | | **Поведінка черги** | Повністю вичерпується за цикл | Одна задача за цикл | | **Коли використовувати** | Оновлення стану, DOM-читання, валідація | Затримки, I/O, взаємодії користувача | ### Як це працює всередині Рушій JavaScript тримає дві окремі черги. Після очищення call stack запускається `MicrotaskQueue::RunMicrotasks()` і це продовжується поки черга порожня. Тільки потім браузер вирішує чи треба перемалювати сторінку. Тільки потім береться одна макрозадача. Мікрозадачі є частиною поточного контексту виконання. Макрозадачі являють собою нову роботу ззовні. ### Типові помилки **Помилка 1: вважати що `setTimeout(..., 0)` виконується одразу** ```javascript setTimeout(() => console.log('швидко'), 0); console.log('повільно'); // Вивід: повільно, швидко // setTimeout - макрозадача. Вона завжди чекає поки вичерпається черга мікрозадач. ``` Якщо потрібно виконати щось одразу після поточного скрипту, використовуй `Promise.resolve().then()` або `queueMicrotask()`, а не `setTimeout`. **Помилка 2: рекурсивні мікрозадачі заморожують UI** ```javascript // Погано: браузер ніколи не отримує черги на рендер function starveUI() { function loop() { Promise.resolve().then(loop); // ставить себе у мікрочергу } loop(); } // Виправлення: поступайся через макрозадачу function safeLoop() { function step() { console.log('step'); setTimeout(step, 0); // між кроками браузер може малювати } step(); } ``` **Помилка 3: думати що `await` в циклі поступається браузеру** ```javascript // Неправильно: await resolved Promise - це мікрозадача, рендер не відбудеться async function processItems(items) { for (const item of items) { await Promise.resolve(); // мікрозадача, рендеринг НЕ відбувається expensiveCalculation(item); } } // Правильно: yield через setTimeout, тоді браузер встигає намалювати кадр async function processItems(items) { for (const item of items) { await new Promise(r => setTimeout(r, 0)); // макрозадача, браузер може рендерити expensiveCalculation(item); } } ``` **Помилка 4: ігнорувати вартість MutationObserver у щільних циклах** ```javascript // Погано: 1000 мутацій DOM = 1000 колбеків у мікрочерзі до рендеру const observer = new MutationObserver(() => updateUI()); for (let i = 0; i < 1000; i++) { element.textContent = i; } // Виправлення: одна зміна DOM = один колбек element.textContent = 999; ``` **Помилка 5: вважати що Node.js і браузер поводяться однаково** У браузерах і Node.js v11+ мікрозадачі завжди виконуються перед наступною макрозадачею. У Node.js v10 і раніше колбеки таймерів могли виконуватись раніше мікрозадач в певних фазах циклу подій. Якщо підтримуєш старіші версії Node, перевіряй поведінку там, а не роби припущення. ### Де це використовується - **React:** збирає кілька `setState` через `Promise.resolve().then()` і застосовує їх одним рендером замість кількох - **Vue:** реактивні оновлення проходять через `Promise.then()` з тієї самої причини - **Node.js streams:** між порціями даних використовуються макрозадачі щоб backpressure не блокував цикл подій - **Intersection Observer:** колбеки виконуються як макрозадачі, тому браузер встигає малювати між спостереженнями - **Express:** async-middleware виконується в черзі мікрозадач у рамках одного запиту На практиці баг зі starvation зустрічається в непомітній формі: пишеш `await somePromise` в циклі й думаєш що поступаєшся браузеру, але якщо Promise вже resolved, це мікрозадача. Рендер так і не відбудеться. ### Питання на співбесіді **Q:** Чому мікрозадачі виконуються раніше макрозадач? **A:** Мікрозадачі є роботою поточного контексту виконання (Promise, що вирішується після синхронного коду). Макрозадачі являють нову роботу ззовні (таймер спрацював, прийшла відповідь мережі). Вичерпати мікрозадачі першими означає завершити весь поточний async-код до того як братися за щось нове. **Q:** Що відбувається якщо мікрозадача ставить у чергу іншу мікрозадачу? **A:** Вона виконується в тому ж циклі. Event loop не переходить до макрозадач поки мікрочерга не спустіє повністю. Саме так і виникає starvation. **Q:** Як `async/await` вписується в цю картину? **A:** `async/await` є синтаксисом над Promise. Коли ти пишеш `await`, функція зупиняється і її залишок ставиться в мікрочергу. Вона відновлюється як мікрозадача, не як макрозадача. **Q:** У чому різниця між `queueMicrotask()` і `Promise.resolve().then()`? **A:** Обидва ставлять колбек у ту ж мікрочергу. `queueMicrotask()` не створює об'єкт Promise, тому трохи дешевший. Використовуй його коли не потрібні chaining або обробка помилок. **Q:** (Senior) React-застосунок пропускає кадри під час великого оновлення даних. Підозра на накопичення мікрозадач. Що робиш? **A:** Переносиш дорогу роботу в макрозадачі через `setTimeout(..., 0)`, браузер встигає рендерити між порціями. У React 18+ обгортаєш несрочні оновлення в `startTransition()`, що дозволяє React переривати роботу і поступатися браузеру. Для точного контролю `requestIdleCallback()` запускає роботу тільки коли браузер вільний, а `requestAnimationFrame()` прив'язує роботу до циклу відмальовування. ## Приклади ### Базовий: порядок виконання ```javascript console.log('start'); setTimeout(() => console.log('timeout'), 0); Promise.resolve() .then(() => console.log('promise 1')) .then(() => console.log('promise 2')); console.log('end'); // Вивід: // start // end // promise 1 // promise 2 // timeout ``` Обидва `.then()` виконуються до `setTimeout` тому що кожен наступний `.then()` в ланцюжку ставить нову мікрозадачу одразу після вирішення попередньої. Весь ланцюжок вичерпується до того як макрозадача отримує чергу. ### Middle: батчинг стану в React ```javascript function handleClick() { setCount(c => c + 1); // ставиться в мікрочергу setCount(c => c + 1); // ставиться в мікрочергу console.log('обробник виконано'); // стан ще не оновився в цей момент } // React збирає обидва оновлення в мікрочерзі // і застосовує їх разом після завершення обробника. // Результат: один ре-рендер замість двох. ``` React використовує `Promise.resolve().then()` всередині щоб зібрати всі зміни стану з одного обробника події і застосувати їх за один прохід рендеру. Без цього кожен `setCount` викликав би окремий рендер. ### Senior: starvation мікрозадач і правильне yielding ```javascript // Погано: повністю заморожує браузер function starve() { function tick() { Promise.resolve().then(tick); } tick(); // Кожна мікрозадача ставить наступну. // Рендер ніколи не відбувається. } // Краще: поступається браузеру між ітераціями function safeYield() { function tick() { // якась робота setTimeout(tick, 0); // браузер може перемалювати між викликами } tick(); } // React 18+: startTransition для несрочних оновлень import { startTransition } from 'react'; startTransition(() => { setItems(buildLargeList()); // React може призупинити і намалювати проміжний стан }); ``` `startTransition` позначає оновлення як несрочне. React може перервати роботу посередині, намалювати те що є, і продовжити. Сторінка залишається відзивчивою замість зависання на весь час обчислення.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.