Какие есть очереди в Event Loop кроме micro и macro tasks?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный вопрос, который затрагивает самую суть асинхронного выполнения в JavaScript. Вы правы, классификация на microtask и macrotask (чаще называемые просто tasks) — это фундаментальная модель. Однако, чтобы дать полный ответ, нужно разобрать устройство Event Loop глубже и посмотреть, как в эту модель встраиваются другие механизмы.
Если коротко: формально в спецификации Event Loop есть только задачи (tasks) и микро-задачи (microtasks). Но на практике, для понимания порядка выполнения, мы часто мысленно выделяем дополнительные "очереди" или, точнее, источники задач (task sources) с разными приоритетами и поведением. Вот как это выглядит в деталях.
📚 Базовая модель: Task и Microtask
Для начала освежим основы. Спецификация WHATWG описывает Event Loop так:
- Task (Macrotask): Крупная единица работы. Каждый цикл Event Loop выбирает и выполняет ровно одну задачу из очереди задач. К ним относятся:
* `setTimeout` и `setInterval`
* События DOM (`click`, `keypress`, `load`)
* `requestAnimationFrame` (хотя его поведение особое, об этом ниже)
* Сетевые события, `fetch` (ответ попадает в задачу)
* Ввод-вывод (I/O)
* `postMessage`
* Код в `<script>` (выполнение основного скрипта)
- Microtask: Мелкая единица работы, выполняемая сразу после текущей задачи, до рендеринга и до выбора следующей задачи. Очередь микро-задач должна быть полностью опустошена. К ним относятся:
* `Promise.then/catch/finally`
* `queueMicrotask()`
* `MutationObserver` (для наблюдения за изменениями DOM)
Классический пример порядка:
console.log('Script start'); // Task
setTimeout(() => console.log('setTimeout'), 0); // Task -> в очередь задач
Promise.resolve()
.then(() => console.log('Promise 1')) // Microtask -> в очередь микро-задач
.then(() => console.log('Promise 2')); // Microtask -> в очередь микро-задач
console.log('Script end'); // Task
// Порядок вывода:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout
🎯 "Скрытые" очереди и источники задач
Теперь к сути вашего вопроса. Помимо явного разделения на task/microtask, в браузерах существуют внутренние механизмы планирования, которые можно условно считать отдельными "очередями" с точки зрения приоритета.
1. Очередь отложенных колбэков requestAnimationFrame (rAF)
requestAnimationFrame — это не макро- и не микро-and задача в чистом виде. Это хук на фазу рендеринга в цикле событий.
- Его колбэки выполняются перед репайнтом и рекомпозицией браузера, в специальном шаге "Update the rendering".
- Они имеют более высокий приоритет, чем задачи от
setTimeout, запланированные с аналогичной задержкой, потому что браузер старается синхронизировать их с кадром анимации (обычно 60 FPS). - Важно: Если внутри колбэка
rAFсгенерируется микро-задача (например,Promise), она выполнится сразу, как и положено, до следующего этапа цикла.
setTimeout(() => console.log('Timeout'), 0); // Task
requestAnimationFrame(() => console.log('rAF')); // Callback в шаге рендеринга
// Порядок может быть разным, но чаще при нулевой задержке:
// rAF (т.к. вызывается перед рендерингом в текущем цикле)
// Timeout (в следующем цикле Event Loop)
2. Очередь сообщений postMessage (MessageChannel)
postMessage и MessageChannel создают задачи, но они часто имеют более высокий приоритет, чем некоторые другие источники задач (например, таймеры), потому что они предназначены для коммуникации и должны быть отзывчивыми. Это не строгое правило спецификации, а особенность реализации в движках.
3. Очередь событий ввода (Input Events)
События от мыши, клавиатуры, касаний (особенно pointermove, scroll) часто планируются в отдельные, высокоприоритетные очереди. Браузер старается обработать их как можно быстрее для обеспечения отзывчивого интерфейса. Они являются задачами (tasks), но могут "обгонять" в очереди, например, колбэки от setTimeout.
4. Очередь requestIdleCallback
Это полная противоположность rAF. Его колбэки планируются на выполнение в периоды простоя браузера, когда основная очередь задач пуста и нет срочных задач рендеринга или ввода. Это самая низкоприоритетная "очередь".
requestIdleCallback((deadline) => {
console.log('Idle callback. Time remaining:', deadline.timeRemaining());
// Здесь выполняем не критичные фоновые задачи
});
🎨 Приоритеты в современном Event Loop (браузер)
Если обобщить и расположить условные "очереди" по приоритету выполнения в одном цикле или между циклами, картина будет такой:
- Текущая выполняющаяся задача (например, синхронный код).
- Вся очередь микро–задач (выполняется до очистки).
- Шаг рендеринга (если требуется):
* Вызов колбэков `requestAnimationFrame`.
* Расчет стилей, layout, paint.
- Задачи (Tasks) — здесь уже работает внутренний планировщик браузера, который может отдавать приоритет:
* **Критические задачи:** События ввода (`click`, `pointerdown`).
* **Задачи передачи данных:** `postMessage`, завершенные `fetch`.
* **Отложенные задачи:** `setTimeout`, `setInterval`.
* **Задачи простоя:** `requestIdleCallback` (выполняются в последнюю очередь).
💎 Вывод
Так что, формально спецификация говорит только о задачах (tasks) и микро-задачах (microtasks). Но для глубокого понимания производительности и предсказания порядка выполнения в реальном браузере необходимо учитывать, что:
requestAnimationFrame— это не задача, а хук на рендеринг.requestIdleCallback— это задача самого низкого приоритета.- Браузерные движки (Blink, Gecko, WebKit) используют внутренний планировщик, который разбивает общую очередь задач на несколько приоритетных подочередей (для событий, таймеров, сетевых операций), чтобы интерфейс оставался плавным и отзывчивым.
Поэтому, отвечая на ваш вопрос: кроме микро- и макро-задач, для полной картины необходимо учитывать очередь рендеринга (rAF) и очередь простоя (requestIdleCallback), а также помнить о внутренней приоритизации задач браузером.