Микро- и макро-таски (tasks) в JavaScript: порядок
В чем разница между микро и макро-тасками в JavaScript?
Теория: что и когда выполняется
JavaScript выполняет синхронный код «до конца» (пока стек вызовов не опустеет), а всё асинхронное раскладывается по очередям и затем подхватывается циклом событий (event loop).
Существует как минимум две разные очереди: очередь task (обычные задачи, иногда их называют «макро-тасками») и очередь microtask (микрозадачи).
task (задача) и microtask (микрозадача); слово «макро-таска» — распространённое разговорное обозначение обычной task.Task — это то, что планируется механизмами платформы: старт выполнения скрипта, обработка событий, срабатывание setTimeout()/setInterval() и т.п.; всё это попадает в очередь задач (task queue).Microtask — это короткая функция, которая выполняется после того, как завершится код, который её создал, и когда стек вызовов пуст, но при этом — до того, как управление вернётся в event loop для выбора следующей task.
Ключевое правило порядка такое: после завершения одной task (например, выполнения всего «тела» скрипта или колбэка таймера) выполняются microtask до полного опустошения очереди microtask, и только потом начинается следующая task.
Если microtask во время выполнения добавляет ещё microtask, они также выполняются в этом же проходе до полного опустошения очереди; процесс продолжается, пока очередь не станет пустой.
microtask могут бесконечно порождать новые microtask, существует риск «захватить» поток выполнения и надолго не дать event loop перейти к следующей task (таймерам, событиям, а также действиям, связанным с обновлением интерфейса).Упрощённая схема одного тика
[синхронный код / одна task]
|
v
[очистка microtask queue до пустоты]
|
v
[взятие следующей task из task queue]
|
...
Важно: «приоритет» microtask проявляется не потому, что они «важнее по времени», а потому что платформа обязана очистить очередь microtask перед тем, как брать следующую task.
Таблица: типичные источники задач
| Что планирует выполнение | Куда попадает | Примеры |
|---|---|---|
| Таймеры/интервалы | task queue | setTimeout(fn, 0), setInterval(fn, 1000) |
| События и их обработчики | task queue | обработка click, message и т.п. как отдельные задачи |
| Реакции промисов | microtask queue | Promise.resolve().then(...), .catch(...), .finally(...) |
| Явная постановка микрозадачи | microtask queue | queueMicrotask(() => ...) |
| Некоторые Web API | microtask queue | например, колбэки Mutation Observer |
Примеры: Promise.then vs setTimeout(0)
Пример 1 — ожидается порядок: синхронный код → microtask → таймеры.
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
Ожидаемый порядок вывода: start, end, promise, timeout.
Причина: обработчик .then(...) попадает в очередь microtask и выполняется после завершения текущего синхронного кода, а setTimeout ставит колбэк в очередь task и будет взят только после очистки очереди microtask.
Пример 2 — microtask внутри task: микрозадача «вклинивается» между задачами, но не прерывает текущий синхронный участок.
setTimeout(() => {
console.log('timer-1 start');
queueMicrotask(() => {
console.log('microtask inside timer-1');
});
console.log('timer-1 end');
}, 0);
setTimeout(() => {
console.log('timer-2');
}, 0);
Ожидаемая логика: сначала выполняется одна timer-task (здесь: timer-1 start, затем timer-1 end), далее перед тем как перейти ко второй timer-task, очередь microtask очищается, поэтому строка microtask inside timer-1 должна появиться до timer-2.
Пример 3 — опасная ситуация: microtask могут «голодать» задачи, если постоянно добавлять новые microtask.
let i = 0;
function spinMicrotasks() {
queueMicrotask(() => {
i += 1;
if (i < 1_000_000) {
spinMicrotasks();
}
});
}
spinMicrotasks();
setTimeout(() => {
console.log('timeout after microtasks');
}, 0);
Смысл примера: setTimeout будет ждать, пока очередь microtask не опустеет; если очередь долго не пустеет из‑за самопополнения, таймер (и другие задачи) могут выполняться с большой задержкой.
Итого: корректное утверждение — 4; microtask (обработчики промисов, queueMicrotask) выполняются сразу после завершения текущего синхронного кода и полностью очищаются перед переходом к следующей task (например, setTimeout).