Микро таски и макро таски - это два способа выполнения задач в JavaScript. Микро таски используются для выполнения задач, которые должны быть выполнены сразу после текущего кода.
Javascript работает в одном потоке. Это значит, что он не может делать две вещи одновременно — он их делает по очереди. Но при этом умудряется скачивать файлы, ждать ответа от сервера, реагировать на клики и рисовать анимацию так, будто у него всё параллельно. Это не магия — это очередь задач.
Представь, что у тебя есть один официант в кафе. Он не может одновременно принимать заказ у одного стола и нести еду другому. Но он может принять заказ, передать его на кухню, пока еда готовится — пойти к следующему столу, потом вернуться за готовым блюдом. Он движется по списку дел, не застревая на одном месте. Javascript работает так же.
Когда ты пишешь setTimeout(() => console.log('Hello'), 1000), движок не останавливается на секунду и не ждёт. Он говорит браузеру: «Запусти таймер, а когда пройдёт секунда — положи эту функцию в очередь». И идёт дальше. Через секунду функция попадает в очередь, и когда Javascript освободится — он её выполнит.
Вот простой пример, который сбивает с толку новичков:
console.log('Первое');
setTimeout(() => console.log('Второе'), 0);
console.log('Третье');Что выведется? «Первое», «Третье», «Второе». Хотя задержка нулевая. Потому что setTimeout — это асинхронная операция. Даже с нулевой задержкой колбэк отправляется в очередь и выполнится только после того, как текущий код закончит работу.
Очередь — это не одна штука, их несколько. Есть макрозадачи (macrotasks) и микрозадачи (microtasks). Макрозадачи — это setTimeout, setInterval, события вроде кликов. Микрозадачи — это промисы и MutationObserver. Разница между ними — в приоритете.
После каждой макрозадачи движок выполняет все микрозадачи, которые накопились. Все до единой. Только потом берётся за следующую макрозадачу. Это как если бы официант после каждого заказа проверял, не нужно ли срочно что-то донести к уже обслуженным столам, и только потом шёл к новому.
console.log('Начало');
setTimeout(() => console.log('Таймаут'), 0);
Promise.resolve().then(() => console.log('Промис'));
console.log('Конец');Вывод: «Начало», «Конец», «Промис», «Таймаут». Текущий код выполнился. Потом все микрозадачи — промис. Потом макрозадачи — таймаут.
Это объясняет странное поведение, которое я однажды встретил в проекте. Был цикл, который создавал сотни промисов, каждый из которых добавлял микрозадачу. Страница зависала, хотя казалось, что всё асинхронное. Проблема в том, что микрозадачи выполняются до следующей макрозадачи. Если их слишком много — движок не может перейти к отрисовке, к обработке кликов, вообще ни к чему. Он застрял в микрозадачах.
Решение было простое — разбить работу на макрозадачи через setTimeout. Тогда между частями работы движок успевал обрабатывать интерфейс.
Есть ещё одна тонкость — рендеринг. Браузер перерисовывает страницу не после каждой строчки кода, а когда Javascript отдаёт управление. То есть после выполнения макрозадачи и всех микрозадач. Если ты меняешь DOM в цикле — пользователь увидит только финальное состояние.
const box = document.querySelector('.box');
box.style.left = '100px';
box.style.left = '200px';
box.style.left = '300px';Блок сразу окажется на позиции 300px, промежуточные значения не отрисуются. Браузер умный — зачем рисовать то, что сразу изменится?
Если нужно увидеть промежуточные шаги — разнести изменения по разным макрозадачам или использовать requestAnimationFrame, которая синхронизируется с частотой обновления экрана.
const box = document.querySelector('.box');
box.style.left = '100px';
setTimeout(() => { box.style.left = '200px' }, 10);
setTimeout(() => { box.style.left = '300px' }, 20);Теперь будет три шага, три отрисовки. Не идеально для анимаций, но принцип понятен.
Event loop — это механизм, который управляет всей этой историей. Он бесконечно крутится: взял макрозадачу из очереди, выполнил, выполнил все микрозадачи, проверил, не пора ли перерисовать экран, взял следующую макрозадачу. И так по кругу.
Когда очередь пустая — движок ждёт. Не тратит процессор, не жужжит. Как только что-то появляется — оживает и обрабатывает.
Понимание очереди задач объясняет почти все странности Javascript. Почему промис резолвится раньше таймаута с нулевой задержкой. Почему долгий синхронный код блокирует интерфейс. Почему изменения DOM батчатся. Почему нельзя вечно генерировать промисы в цикле.
Это не сложная абстракция — это просто список дел, который движок обрабатывает по правилам. Одна задача за раз, микрозадачи в приоритете, рендеринг между макрозадачами. Всё остальное — следствия этих правил.
И когда в следующий раз код поведёт себя неожиданно — не спеши в баг-репорты. Возможно, это просто очередь задач делает свою работу.
Alex Moore
Full-stack Developer