Какой механизм браузера отвечает за порядок выполнения микротасков и макротасков в JavaScript
Что проверяют
Название знают многие: event loop. Проверяют, можете ли вы по коду предсказать порядок вывода и понимаете ли, где в цикле место рендеру. Следом дают задачку с setTimeout, Promise, async/await, иногда с requestAnimationFrame.
Как отвечать
Механизм называется событийный цикл, event loop. Важная деталь: его описывает не спецификация ECMAScript, а HTML Standard, поэтому в браузере и в Node.js он устроен по-разному. Слова «макротаска» в спецификации нет, там задачи (tasks) и микрозадачи (microtasks).
Один оборот цикла в браузере:
- задача: из очереди берётся одна задача и выполняется до конца. Это колбэк
setTimeoutиsetInterval, обработчик события,postMessage, выполнение скрипта; - микрозадачи: когда стек вызовов опустел, выполняется вся очередь микрозадач, включая добавленные по ходу. Сюда попадают
then,catch,finallyу промисов, продолжение послеawait,queueMicrotaskиMutationObserver; - рендер: если браузер решил отрисовать кадр, выполняются колбэки
requestAnimationFrame, затем расчёт стилей, раскладка и отрисовка. Это не каждый оборот, а примерно раз в кадр; - следующая задача: и цикл повторяется.
Отсюда классика: сначала синхронные console.log, затем все then, и только потом setTimeout(..., 0). Нулевой таймер не значит «сразу», а при глубокой вложенности браузер поднимает задержку минимум до 4 мс.
Для Node.js отметьте отличия: цикл реализует libuv по фазам (таймеры, ввод-вывод, setImmediate), а process.nextTick выполняется раньше промисов.
Практический вывод: длинную синхронную работу дробят на задачи, чтобы между ними успевали пройти рендер и ввод пользователя. Для этого подходят setTimeout, MessageChannel и, где поддерживается, scheduler.yield().
Где спотыкаются
Считают, что микрозадачи выполняются по одной между задачами. Очередь вычищается целиком, и бесконечная цепочка микрозадач вешает страницу сильнее любого таймера: рендер не наступает вообще.
Относят requestAnimationFrame к макрозадачам. Он живёт в фазе рендера и привязан к частоте кадров.
Забывают, что код async-функции до первого await выполняется синхронно, и ошибаются в порядке вывода.