Микрозадачи и макрозадачи #
JavaScript выполняет код в одном потоке, а отложенную работу ставит в очереди. Две важные категории — микрозадачи (microtasks) и макрозадачи (macrotasks, task). Понимание порядка объясняет, почему then срабатывает раньше setTimeout, и помогает избегать гонок в UI.
Один цикл за раз #
Пока выполняется синхронный код, очереди не продвигаются. После текущего «куска» движок:
- Обрабатывает все микрозадачи из очереди (до опустошения).
- Берёт одну макрозадачу.
- Снова обрабатывает все микрозадачи.
- И так далее — модель event loop.
Что куда попадает #
| Тип | Примеры |
|---|---|
| Микрозадача | колбэки .then / .catch / .finally, queueMicrotask, await (продолжение async-функции) |
| Макрозадача | setTimeout, setInterval, события DOM, часть I/O в Node |
requestAnimationFrame в браузере — отдельная фаза перед отрисовкой; не путайте с микрозадачами, но помните: он ближе к кадру, чем setTimeout(0).
Классический пример порядка #
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
Синхронный код завершается, затем опустошается очередь микрозадач (промис), и только потом — таймер.
Цепочка микрозадач #
Каждый .then добавляет новую микрозадачу; они выполняются подряд до следующей макрозадачи:
Promise.resolve()
.then(() => console.log('A'))
.then(() => console.log('B'))
setTimeout(() => console.log('C'), 0)
// A → B → C
Длинные цепочки промисов без await между кадрами могут отложить отрисовку — в тяжёлых UI разбивайте работу на макрозадачи (setTimeout, requestAnimationFrame) или чанки.
async/await и микрозадачи #
await приостанавливает async-функцию; продолжение после разрешения промиса — микрозадача:
async function demo() {
console.log('start')
await null
console.log('after await')
}
demo()
console.log('sync end')
// start → sync end → after await
queueMicrotask #
Явная постановка микрозадачи:
queueMicrotask(() => {
console.log('обновить счётчик без setTimeout(0)')
})
Предпочтительнее setTimeout(fn, 0), когда нужно выполнить код сразу после текущего синхронного блока, но до следующей макрозадачи.
Практический смысл #
- Обновление DOM после промиса — выполнится до таймеров с нулевой задержкой; тесты на
setTimeout(0)могут быть хрупкими. - Бесконечные микрозадачи (
then→then→ … без пауз) заблокируют обработку кликов и таймеров — starvation макрозадач. - Ошибка в микрозадаче без
.catch—unhandledrejection; в синхронном коде — обычныйtry/catchне поймает.
Promise.resolve().then(() => {
throw new Error('в микрозадаче')
})
// try/catch вокруг этого блока не сработает
Итог #
- После синхронного кода движок сначала выполняет все микрозадачи, затем одну макрозадачу.
- Промисы и
await— микрозадачи;setTimeout— макрозадача. - Порядок
sync → microtasks → macrotaskобъясняет большинство «загадочных» логов в консоли. - Не создавайте бесконечный поток микрозадач; для уступки потоку используйте макрозадачи или разбиение работы.