Почему setTimeout выполняется после микрозадач в JavaScript
Что проверяют
Знаете ли вы, что очередь микрозадач опустошается целиком между задачами, и умеете ли предсказать порядок вывода. Следом дают код с await, Promise.then и setTimeout и просят расставить порядок выполнения.
Как отвечать
Опишите одну итерацию цикла событий, из неё всё и следует.
- шаг цикла: движок берёт одну задачу (макрозадачу), выполняет её до конца, затем полностью опустошает очередь микрозадач, и только после этого браузер может пересчитать стили и отрисовать кадр;
- микрозадачи: колбэки промисов, продолжение функции после
await,queueMicrotask,MutationObserver. Новые микрозадачи, порождённые внутри обработки, выполняются в этом же проходе; - макрозадачи: колбэки
setTimeoutиsetInterval, события ввода,postMessage, завершение сетевых операций; - вывод: колбэк
setTimeoutпопадает в очередь задач только после срабатывания таймера, а очередь разбирается после текущего синхронного кода и всех микрозадач. Отсюда порядок: синхронный код,.then, таймер.
Добавьте детали, которые отличают понятый ответ от заученного. Ноль в setTimeout означает не «сейчас», а «не раньше чем»: спецификация требует минимум 4 мс при вложенности таймеров больше пяти, а реальную задержку определяет занятость потока. Бесконечная цепочка микрозадач подвешивает вкладку: до следующей задачи и до отрисовки управление не доходит. requestAnimationFrame не задача из общей очереди, его колбэк вызывается перед отрисовкой кадра.
В Node картина похожая, но своя: очередь process.nextTick разбирается раньше промисов, а таймеры живут в отдельной фазе рядом с setImmediate, и порядок setTimeout(fn, 0) против setImmediate на верхнем уровне не детерминирован.
Где спотыкаются
Говорят «микрозадачи приоритетнее» и не могут раскрыть механизм. Дело не в приоритете, а в том, что очередь микрозадач вычерпывается до дна после каждой задачи.
Считают, что setTimeout(fn, 0) выполнится немедленно, и строят на этом логику, которая ломается под нагрузкой.
Забывают, что await разрезает функцию на части. Код после await это микрозадача, поэтому он отработает раньше любого таймера, даже поставленного до него.