← Назад к вопросам

Почему Promise выполняется раньше setTimeout?

2.2 Middle🔥 271 комментариев
#JavaScript Core

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI4 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Приоритет выполнения: Promise vs setTimeout

Этот вопрос затрагивает ключевые аспекты устройства Event Loop в JavaScript, которые определяют порядок выполнения асинхронных операций. Короткий ответ: Promise (если он уже resolved) выполняется в текущей или следующей микро-таске (microtask), а setTimeout — в макро-таске (macrotask), и Event Loop обрабатывает все микро-таски перед переходом к следующей макро-таске. Давайте разберем это подробно.

Event Loop и две очереди задач

JavaScript имеет однопоточную модель выполнения, но использует Event Loop для обработки асинхронности. Event Loop управляет двумя основными типами очередей задач:

  • Макро-таски (macrotasks или просто tasks): К ним относятся setTimeout, setInterval, I/O операции (например, чтение файлов), события UI (клики, скролл) и setImmediate (в Node.js).
  • Микро-таски (microtasks): К ним относятся callbackы Promise (.then(), .catch(), .finally()), queueMicrotask(), process.nextTick() (в Node.js) и некоторые методы, связанные с MutationObserver.

Ключевое правило Event Loop

После выполнения каждой макро-таски Event Loop проверяет очередь микро-тасков и выполняет ВСЕ имеющиеся в ней задачи до того, как перейти к следующей макро-таске. Это фундаментальное правило объясняет наблюдаемое поведение.

Рассмотрим классический пример:

console.log('1. Начало синхронного кода');

setTimeout(() => {
    console.log('4. setTimeout (макро-таска)');
}, 0);

Promise.resolve()
    .then(() => {
        console.log('3. Promise.then (микро-таска)');
    });

console.log('2. Конец синхронного кода');

Вывод будет:

1. Начало синхронного кода
2. Конец синхронного кода
3. Promise.then (микро-таска)
4. setTimeout (макро-таска)

Последовательность выполнения шаг за шагом

  1. Синхронная фаза: Выполняются первые две строки console.log. Это синхронный код, он выполняется немедленно.
  2. Планирование асинхронных задач:
    *   `setTimeout` регистрирует свою callback-функцию в **очереди макро-тасков**. Даже с нулевой задержкой, он не выполняется сразу — минимальная задержка зависит от реализации браузера/Node.js (обычно ~4ms), и callback попадает в очередь макро-тасков.
    *   `Promise.resolve()` создает **немедленно resolved Promise**. Его callback (`.then()`) помещается в **очередь микро-тасков**.
  1. Event Loop начинает работу:
    *   После завершения синхронного кода (первой макро-таски — самого скрипта), Event Loop проверяет очередь **микро-тасков**.
    *   Он обнаруживает там callback от Promise и **выполняет его** (вывод `3. Promise.then...`).
    *   Очередь микро-тасков пуста.
  1. Переход к следующей макро-таске:
    *   Event Loop теперь переходит к **очереди макро-тасков**.
    *   Он берет callback от `setTimeout` и выполняет его (вывод `4. setTimeout...`).

Почему это важно на практике?

  • Гарантия порядка в рамках одной логической операции: Если в процессе обработки одной макро-таски (например, клика) генерируются несколько микро-таск (например, обновления состояния через Promise), они будут выполнены непрерывно, до того как браузер перейдет к рендерингу или другой макро-таске. Это позволяет избежать "разрывов" в состоянии UI.
  • Рендеринг браузера: Браузер часто рендерит изменения (перерисовывает страницу) между макро-тасками. Выполнение всех микро-таск перед рендерингом гарантирует, что все подготовленные данные (например, через Promise) уже применены и UI будет отображен корректно.
  • Потенциальная блокировка: Бесконечное добавление микро-таск (например, рекурсивные .then()) может заблокировать Event Loop, поскольку он будет пытаться очистить очередь микро-таск до перехода к следующей макро-таске. Это может заморозить интерфейс.

Дополнительный пример с уже resolved Promise

setTimeout(() => console.log('макро-таска 1'), 0);

Promise.resolve()
    .then(() => {
        console.log('микро-таска 1');
        // Добавляем еще одну микро-таску внутри первой
        Promise.resolve().then(() => console.log('микро-таска 2 (вложенная)'));
    });

setTimeout(() => console.log('макро-таска 2'), 0);

Вывод:

микро-таска 1
микро-таска 2 (вложенная)
макро-таска 1
макро-таска 2

Event Loop выполнил обе микро-таски (включая вложенную) перед тем, как перейти к первой макро-таске (setTimeout).

Исключения и нюансы

  • Promise, который еще не resolved: Если Promise создан, но не разрешен (например, ожидает сетевого запроса), его .then() не попадет в очередь микро-таск сразу, и может выполниться после setTimeout, который был запланирован позже, но уже попал в очередь макро-таск.
  • setTimeout с нулевой задержкой: Не гарантирует мгновенное выполнение. Он лишь помещает callback в очередь макро-таск как можно скорее, но после текущих и всех микро-таск.

Таким образом, Promise выполняется раньше setTimeout не потому, что он "быстрее" или "приоритетнее" в абсолютном смысле, а потому, что задачи из очереди микро-таск обрабатываются Event Loop между макро-тасками, и это жесткое правило его архитектуры. Для разработчика это означает необходимость четко понимать тип своей асинхронной операции (микро- или макро-таска) при планировании последовательности выполнения кода.

Почему Promise выполняется раньше setTimeout? | PrepBro