Загружаем научный разбор
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.
Автор: Казачкин Даниил Михайлович · Обновлено
Причинная модель очередности Promise и таймеров: границы ECMAScript и HTML, контролируемые трассы, готовность реакции и ограничения выводов о времени исполнения.
Фраза «микрозадачи приоритетнее макрозадач» помогает угадать простой вывод в консоли, но плохо объясняет реальную программу. Она ничего не говорит о моменте постановки обработчика в очередь, состоянии промиса и том, кто вообще управляет очередями. Особенно легко ошибиться, если принять приоритет за способность прервать уже работающую функцию. В таком представлении любой Promise должен немедленно обгонять любой таймер. Достаточно отложить разрешение промиса, чтобы это предсказание перестало работать.
Здесь мы построим проверяемую модель: сначала сформулируем условия, затем предскажем последовательность наблюдений и изменим одно условие. Это авторский разбор стандартов с небольшими демонстрационными экспериментами, а не исследование быстродействия движков. Базовые упражнения с синтаксисом вынесены в [урок об event loop](/lessons/javascript/javascript-for-developers/javascript-event-loop). Для чтения достаточно понимать вызов функции, обработчик then и назначение таймера.
ECMAScript описывает промисы и задания, которые вызывают их реакции. HTML связывает эти задания с микрозадачами браузерной среды и задаёт обработку событий. Поэтому картинка «стек и две очереди внутри JavaScript» полезна лишь как упрощение. У браузерного цикла может быть несколько очередей задач, а очередь микрозадач рассматривается отдельно. Выбор готовой задачи не сводится к одной общей очереди всех событий страницы. ECMAScript: Promise Objects, HTML: Event loops.
Зафиксируем границу нашей модели: один обычный скрипт в документе браузера, встроенный Promise, отсутствие стороннего кода, модальных диалогов и вмешательства отладчика. Модель не обещает порядок сообщений между независимыми worker и окнами. Она также не является полным описанием Node.js. Такое ограничение делает вывод сильнее: мы заранее знаем, какие наблюдения обязаны объяснить и какие пока не рассматриваем.
Сохраните фрагмент в обычном HTML-файле внутри одного элемента script и откройте документ. Вместо постоянного вывода собираем короткую трассу в массив. Это уменьшает влияние консоли на наблюдение и позволяет сравнивать ровно одну строку. До запуска выпишите ожидаемый порядок меток, включая вложенную микрозадачу.
const trace = [];
trace.push('start');
setTimeout(() => {
trace.push('timer');
console.log(trace.join(' → '));
}, 0);
Promise.resolve().then(() => {
trace.push('promise');
queueMicrotask(() => trace.push('nested'));
});
queueMicrotask(() => trace.push('microtask'));
trace.push('end');Предсказание: start → end → promise → microtask → nested → timer. Существенна здесь не длительность ожидания, а причинная последовательность. На синхронном участке появляются start и end. К его завершению реакция уже выполненного промиса и явно добавленная микрозадача ожидают обработки. Вложенная работа ещё не поставлена: соответствующая строка находится внутри будущего обработчика.
Когда вызывается обработчик промиса, он добавляет nested после уже ожидающей microtask. Поэтому объяснение «всё внутри Promise выполнится раньше queueMicrotask» неверно. Важен момент добавления конкретной работы, а не визуальная вложенность строк. Проверяемая модель предсказывает положение каждой метки; правило по названию API угадывает только часть результата.
Термин «приоритет» провоцирует представление о планировщике операционной системы, который приостанавливает текущую работу ради более важной. Наш опыт демонстрирует другое: end появляется перед promise, хотя промис уже выполнен. Регистрация реакции не заменяет обычный вызов функции и не внедряет обработчик между соседними операторами текущего скрипта.
HTML определяет контрольные точки обработки микрозадач, в том числе после выполнения задачи; сама процедура продолжает извлекать микрозадачи до опустошения очереди. Этим объясняется положение nested перед таймером. Контрольные точки существуют и в других алгоритмах стандарта, поэтому формула «ровно один раз после любой макрозадачи» слишком груба для полного анализа браузера. Для нашего изолированного скрипта достаточно указанной границы. HTML: microtask checkpoint.
Полезно разделить три события: создание объекта Promise, изменение его состояния и выполнение реакции. Между ними не обязательно проходит заметное время, но это разные шаги рассуждения. Вызов функции, переданной конструктору Promise, происходит синхронно. Задание для реакции появляется по правилам обработки промиса. Так можно объяснить трассу, даже если заменить Promise.resolve() конструктором с немедленным resolve, не вводя специального правила «конструктор асинхронный». ECMAScript: Promise constructor.
Теперь изменим только источник готовности. Пусть завершение промиса зависит от таймера. Код сохраняет небольшой размер, однако вывод проверяет более содержательную гипотезу: способен ли механизм микрозадач исполнить обработчик до появления результата?
const trace = [];
let finish;
const pending = new Promise((resolve) => { finish = resolve; });
pending.then(() => {
trace.push('reaction');
console.log(trace.join(' → '));
});
setTimeout(() => {
trace.push('timer');
finish();
trace.push('timer-end');
}, 0);
trace.push('script-end');Ожидаем script-end → timer → timer-end → reaction. Отсутствие метки reaction до таймера не опровергает стандарт. До вызова finish нужная реакция не готова к выполнению. После него обработчик таймера всё равно завершает свой синхронный участок. Объяснение получается из зависимостей, без измерения миллисекунд и без исключения из правила приоритетов.
Практический перенос такого рассуждения — обработка сетевых ответов. Само наличие fetch(...).then(...) не обещает, что обработчик опередит запланированное действие интерфейса. Для этого сначала должна завершиться соответствующая операция. Если два запроса независимы, последовательность их регистрации также не создаёт порядок завершения. Точное отношение нужно строить через зависимости приложения, идентификаторы актуального запроса или отмену, а не через надежду на знакомую очередь.
У setTimeout(..., 0) нет договора «вызвать через ноль миллисекунд». У таймеров есть правила ожидания, постановки задачи и ограничения вложенности; занятая среда может добавить задержку. Поэтому тест, который требует точного значения performance.now(), проверяет нагрузку и условия запуска вместе с логикой. Даже устойчивое число на одном компьютере не превращается в гарантию стандарта. HTML: Timers.
Представим длительную синхронную сортировку после регистрации таймера. Из нашей модели следует, что сортировка завершится до обработки его callback. Ускорение сортировки уменьшит фактическое ожидание, но не поменяет причинный порядок. Это хороший пример различия корректности и производительности: для первой достаточно отношения «раньше», для второй нужна длительность в заданной среде с повторениями и описанием нагрузки.
Аналогично, завершение микрозадачи не является подтверждением показа кадра пользователю. Если код последовательно меняет текст и ожидает уже выполненный Promise, это ещё не доказательство промежуточной отрисовки. Для анализа интерфейса потребуется отдельная модель обновления rendering и наблюдение кадра. Не стоит подменять её трассой из консоли: консоль сообщает об исполнении инструкций, а не о том, какие пиксели успел увидеть человек.
Ещё одна полезная гипотеза звучит так: «если разбить работу на микрозадачи, браузер сможет обработать ввод между кусками». Проверять бесконечную рекурсию опасно для удобства эксперимента, поэтому ограничим число повторов. Возьмём счётчик из ста шагов и таймер, который читает его значение.
let completed = 0;
function step() {
completed += 1;
if (completed < 100) queueMicrotask(step);
}
setTimeout(() => console.log(completed), 0);
queueMicrotask(step);Ожидаемое наблюдение — 100. Между шагами возврат происходит к обработке микрозадач, но очередь снова пополняется. Этот конечный опыт не измеряет заметность задержки ввода: сто пустых операций слишком малы для такого вывода. Зато он опровергает выбранную причинную модель. Масштабирование до тяжёлых вычислений требует другого механизма разделения работы и измерения отзывчивости, а не механической замены цикла цепочкой Promise.
При автоматизированной проверке 8 сентября 2026 года все три фрагмента дали указанные трассы в Chromium 151 и Node.js 24.13.0. Это наблюдение подтверждает предсказания для данных программ в двух средах. Оно не является измерением скорости и не доказывает одинаковое расписание произвольных браузерных и серверных API.
У опыта должен быть и отрицательный контроль: изменение, которое по нашей гипотезе не влияет на порядок. Например, поменяйте строковые метки или увеличьте конечный счётчик со ста до двухсот. Порядок категорий работы должен сохраниться, хотя длительность может измениться. Если тест начинает зависеть от текста метки, вероятно, в стенд попало дополнительное поведение. Отрицательный контроль помогает отличить объясняющий фактор от случайного совпадения и дисциплинирует отчёт: мы проверяем конкретную зависимость, а не просто собираем успешные запуски.
При неожиданном результате сначала сохраните трассу целиком. Затем проверьте, не запускались ли части кода отдельными командами консоли и не оказался ли исходный скрипт модулем с дополнительным ожиданием. Исправление условий опыта может быть важнее повторного чтения схемы очередей.
В журнале опыта сохраните исходный файл, среду, версию браузера и предсказание до запуска. Повторите сценарий после перезагрузки, затем переставьте регистрации первой реакции и queueMicrotask. Предсказание должно измениться только там, где изменился момент постановки. Далее замените выполненный промис на ожидающий: это проверит, что модель учитывает готовность, а не просто узнаёт шаблон задачи с собеседования.
Node.js удобно использовать как дополнительную среду для этих коротких фрагментов, однако совпавший вывод не доказывает тождество всех правил. У Node есть собственные фазы цикла, взаимодействие с вводом-выводом и особая очередь process.nextTick. В документации отдельно рассматривается зависимость поведения таймеров от версии libuv. Для серверного отчёта фиксируйте версию Node и формат запуска файла. Node.js: Event Loop.
В учебном проекте полезнее закончить разбор собственным тестом зависимости, чем коллекцией загадок. Возьмите обновление состояния после Promise, сформулируйте момент, когда результат обязан стать доступен, и проверьте этот контракт. [Урок о промисах](/lessons/javascript/javascript-for-developers/javascript-promises) поможет выразить зависимость в коде, а [урок об async/await](/lessons/javascript/javascript-for-developers/javascript-async-await) — прочитать её без вложенных обработчиков. Проверяемая модель останется той же независимо от выбранного синтаксиса.
Авторский русскоязычный разбор. Перечисленные первичные источники использованы для проверки технических утверждений и библиографии; материал не является переводом или перепечаткой источников. Собственные примеры и рассуждения отделены от опубликованных эмпирических результатов.
Иллюстрации, таблицы и код первоисточников не воспроизводятся. Примеры и текстовые схемы созданы для этой статьи. Ссылки и библиографические сведения не предоставляют прав на перепубликацию материалов источников.