Back

Как работают JavaScript Promises с циклом событий

Как работают JavaScript Promises с циклом событий

Асинхронное поведение JavaScript часто озадачивает разработчиков. Вы пишете setTimeout(() => console.log('timeout'), 0), ожидая немедленного выполнения, но Promise разрешается первым. Понимание того, как JavaScript Promises взаимодействуют с циклом событий (Event Loop), объясняет, почему это происходит, и помогает писать более предсказуемый асинхронный код.

Ключевые выводы

  • Цикл событий обрабатывает все микрозадачи перед переходом к следующей макрозадаче
  • Promises и async/await используют очередь микрозадач, получая приоритет над setTimeout
  • Понимание системы двух очередей помогает предсказать порядок выполнения в JavaScript
  • Рекурсивные микрозадачи могут «заморить голодом» цикл событий и заблокировать макрозадачи

Основы цикла событий JavaScript

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

Стек вызовов выполняет синхронный код немедленно. Когда стек опустошается, цикл событий проверяет наличие ожидающих задач в определённом порядке:

  1. Сначала выполняются все микрозадачи
  2. Выполняется одна макрозадача
  3. Цикл повторяется

Эта система приоритетов объясняет, почему промисы ведут себя иначе, чем setTimeout.

JavaScript: Макрозадачи против микрозадач — критическое различие

Понимание различий между макрозадачами и микрозадачами необходимо для прогнозирования порядка выполнения кода.

К макрозадачам относятся:

  • setTimeout
  • setInterval
  • Операции ввода-вывода (I/O)
  • Рендеринг UI

К микрозадачам относятся:

  • Колбэки Promise (.then, .catch, .finally)
  • queueMicrotask()
  • Колбэки MutationObserver

Цикл событий обрабатывает все микрозадачи перед переходом к следующей макрозадаче. Это создаёт систему приоритетов, где промисы всегда выполняются раньше таймеров.

Promises и очередь микрозадач в действии

Давайте рассмотрим, как промисы взаимодействуют с циклом событий на примере кода:

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));

console.log('5');

Вывод: 1, 5, 3, 4, 2

Вот поток выполнения:

  1. Синхронный console.log('1') выполняется немедленно
  2. setTimeout планирует колбэк в очередь макрозадач
  3. Колбэки Promise помещаются в очередь как микрозадачи
  4. Синхронный console.log('5') выполняется
  5. Цикл событий обрабатывает все микрозадачи (3, 4)
  6. Цикл событий обрабатывает одну макрозадачу (2)

Очередь микрозадач полностью опустошается до выполнения любой макрозадачи, даже при таймаутах с нулевой задержкой.

Интеграция Async/Await с циклом событий

Поведение async/await следует тем же правилам микрозадач. Ключевое слово await приостанавливает выполнение функции и планирует продолжение как микрозадачу:

async function example() {
  console.log('1');
  await Promise.resolve();
  console.log('2');  // Это становится микрозадачей
}

example();
console.log('3');

// Вывод: 1, 3, 2

После await оставшаяся часть тела функции присоединяется к очереди микрозадач. Это объясняет, почему console.log('3') выполняется раньше console.log('2'), несмотря на то, что появляется позже в коде.

Распространённые ошибки и практические паттерны

Истощение очереди микрозадач

Рекурсивное создание микрозадач может заблокировать цикл событий:

function dangerousLoop() {
  Promise.resolve().then(dangerousLoop);
}
// Не делайте так — блокирует все макрозадачи

Смешивание таймеров с промисами

При комбинировании различных асинхронных паттернов помните о приоритете выполнения:

setTimeout(() => console.log('timeout'), 0);

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log('data'));

Promise.resolve().then(() => console.log('immediate'));

// Порядок: immediate → data (когда готово) → timeout

Отладка порядка выполнения

Используйте этот паттерн для трассировки потока выполнения:

console.log('Sync start');

queueMicrotask(() => console.log('Microtask 1'));

setTimeout(() => console.log('Macrotask 1'), 0);

Promise.resolve()
  .then(() => console.log('Microtask 2'))
  .then(() => console.log('Microtask 3'));

setTimeout(() => console.log('Macrotask 2'), 0);

console.log('Sync end');

// Вывод: Sync start, Sync end, Microtask 1, Microtask 2, Microtask 3, Macrotask 1, Macrotask 2

Заключение

Система двух очередей цикла событий определяет порядок асинхронного выполнения в JavaScript. Promises и async/await используют очередь микрозадач, получая приоритет над setTimeout и другими макрозадачами. Эти знания превращают загадочное асинхронное поведение в предсказуемые паттерны, позволяя вам писать более надёжный асинхронный JavaScript-код.

Запомните: синхронный код выполняется первым, затем очищаются все микрозадачи, затем выполняется одна макрозадача. Этот цикл повторяется, позволяя однопоточному JavaScript эффективно обрабатывать сложные асинхронные операции.

Часто задаваемые вопросы

Колбэки Promise являются микрозадачами, в то время как setTimeout создаёт макрозадачи. Цикл событий всегда обрабатывает все микрозадачи перед переходом к следующей макрозадаче, независимо от длительности таймаута.

Да, непрерывное создание промисов или микрозадач без предоставления возможности выполнения макрозадач может истощить цикл событий. Это предотвращает обновление UI и выполнение других макрозадач.

Async/await — это синтаксический сахар для промисов. Ключевое слово await приостанавливает выполнение и планирует продолжение как микрозадачу, следуя тем же правилам приоритета, что и колбэки промисов.

Различные паттерны следуют правилам своих очередей. Promises и async/await используют микрозадачи, в то время как setTimeout и setInterval используют макрозадачи. Микрозадачи всегда выполняются первыми, когда стек вызовов пуст.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay