Что такое Event Loop?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#JavaScript #Асинхронность #База JS

Краткий ответ

Event Loop — это механизм в JavaScript, который отвечает за выполнение кода, сбор и обработку событий, а также выполнение подпрограмм. Он позволяет JavaScript быть однопоточным, но при этом выполнять асинхронные операции, такие как setTimeout, Promise, fetch и другие.

Основные компоненты:

  • Call Stack — стек вызовов функций
  • Callback Queue — очередь обратных вызовов
  • Microtask Queue — очередь микрозадач
  • Web APIs — асинхронные API браузера

Полный ответ

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

Как работает Event Loop

Event Loop работает по следующему циклу:

  1. Выполняет все синхронные операции из стека вызовов
  2. Проверяет очередь микрозадач и выполняет все задачи оттуда
  3. Проверяет очередь обратных вызовов и выполняет одну задачу оттуда
  4. Повторяет цикл
console.log('1');
 
setTimeout(() => console.log('2'), 0);
 
Promise.resolve().then(() => console.log('3'));
 
console.log('4');
 
// Вывод: 1, 4, 3, 2

Основные компоненты

1. Call Stack (Стек вызовов)

Стек, где хранятся вызовы функций:

function first() {
  console.log('first');  // 1. Выполняется console.log('first')
  second();              // 2. Вызов second() - добавляется в стек
}
 
function second() {
  console.log('second'); // 3. Выполняется console.log('second')
  third();               // 4. Вызов third() - добавляется в стек
}
 
function third() {
  console.log('third');  // 5. Выполняется console.log('third')
}                        // 6. third() завершается и удаляется из стека
                         // 7. second() завершается и удаляется из стека
                         // 8. first() завершается и удаляется из стека
 
first();                 // 0. Вызов first() - добавляется в стек

2. Web APIs

Асинхронные API браузера, которые обрабатывают таймеры, события, HTTP-запросы:

setTimeout(() => {
  console.log('Таймер завершен');
}, 1000);
 
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data));

3. Callback Queue (Очередь обратных вызовов)

Очередь для задач типа setTimeout, setInterval, click events:

setTimeout(() => {
  console.log('Из очереди обратных вызовов');
}, 0);

4. Microtask Queue (Очередь микрозадач)

Очередь для Promise, queueMicrotask:

Promise.resolve().then(() => {
  console.log('Из очереди микрозадач');
});

Приоритеты выполнения

Очередь микрозадач имеет более высокий приоритет:

console.log('1');
 
setTimeout(() => console.log('2'), 0);
 
Promise.resolve().then(() => console.log('3'));
 
Promise.resolve().then(() => console.log('4'));
 
setTimeout(() => console.log('5'), 0);
 
console.log('6');
 
// Вывод: 1, 6, 3, 4, 2, 5

Практические примеры

Пример с Promise и setTimeout

console.log('start');
 
setTimeout(() => console.log('setTimeout'), 0);
 
Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'));
 
console.log('end');
 
// Вывод: start, end, promise1, promise2, setTimeout

Пример с вложенными Promise

Promise.resolve()
  .then(() => {
    console.log('promise1');
    return Promise.resolve();
  })
  .then(() => {
    console.log('promise2');
  });
 
Promise.resolve().then(() => console.log('promise3'));
 
// Вывод: promise1, promise3, promise2

Пример с async/await

async function example() {
  console.log('1');
  
  await Promise.resolve();
  
  console.log('2');
}
 
example();
console.log('3');
 
// Вывод: 1, 3, 2

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

1. Непонимание приоритетов

// ❌ Ожидание: 1, 2, 3
// ✅ Реальность: 1, 3, 2
console.log('1');
 
setTimeout(() => console.log('2'), 0);
 
Promise.resolve().then(() => console.log('3'));

2. Блокировка Event Loop

// ❌ Плохо - блокирует Event Loop
function blockingOperation() {
  while (true) {
    // Бесконечный цикл
  }
}
 
// ✅ Хорошо - разбиваем на части
function nonBlockingOperation() {
  let i = 0;
  function processChunk() {
    const end = Math.min(i + 1000, data.length);
    for (; i < end; i++) {
      // Обработка данных
    }
    if (i < data.length) {
      setTimeout(processChunk, 0);
    }
  }
  processChunk();
}

Лучшие практики

  1. Понимайте приоритеты — микрозадачи выполняются перед обратными вызовами
  2. Избегайте блокировки — разбивайте тяжелые операции на части
  3. Используйте Promise — для лучшей управляемости асинхронного кода
  4. Оптимизируйте производительность — минимизируйте количество задач в очередях

Ключевые особенности

  1. Однопоточность — JavaScript выполняется в одном потоке
  2. Неблокирующий I/O — асинхронные операции не блокируют выполнение
  3. Приоритет микрозадач — очередь микрозадач опустошается полностью перед следующей задачей
  4. Предсказуемость — строгий порядок выполнения задач

Event Loop — это сердце асинхронного программирования в JavaScript. Понимание его работы критически важно для написания эффективного и предсказуемого кода, особенно при работе с таймерами, промисами, сетевыми запросами и событиями.


Задачи для проверки знаний

Задача 1

Что будет выведено в консоль?

console.log('A');
 
setTimeout(() => console.log('B'), 0);
 
Promise.resolve().then(() => console.log('C'));
 
console.log('D');
Посмотреть ответ

Ответ: A, D, C, B

Объяснение:

  1. console.log('A') — синхронный вызов, выполняется сразу
  2. setTimeout(() => console.log('B'), 0) — асинхронная задача попадает в Callback Queue
  3. Promise.resolve().then(() => console.log('C')) — микрозадача попадает в Microtask Queue
  4. console.log('D') — синхронный вызов, выполняется сразу
  5. Event Loop проверяет Microtask Queue и выполняет console.log('C')
  6. Event Loop проверяет Callback Queue и выполняет console.log('B')

Задача 2

Какой будет порядок вывода?

setTimeout(() => console.log('A'), 0);
 
Promise.resolve().then(() => {
  console.log('B');
  return Promise.resolve();
}).then(() => console.log('C'));
 
console.log('D');
Посмотреть ответ

Ответ: D, B, C, A

Объяснение:

  1. setTimeout(() => console.log('A'), 0) — задача в Callback Queue
  2. Promise.resolve().then(...) — микрозадача в Microtask Queue
  3. console.log('D') — синхронный вызов
  4. Микрозадача выполняется: вывод ‘B’, создается новая микрозадача для вывода ‘C’
  5. Вторая микрозадача выполняется: вывод ‘C’
  6. Задача из Callback Queue: вывод ‘A’

Задача 3

Что выведет этот код?

async function async1() {
  console.log('A');
  await async2();
  console.log('B');
}
 
async function async2() {
  console.log('C');
}
 
console.log('D');
 
setTimeout(() => console.log('E'), 0);
 
async1();
 
new Promise((resolve) => {
  console.log('F');
  resolve();
}).then(() => console.log('G'));
 
console.log('H');
Посмотреть ответ

Ответ: D, A, C, F, H, G, B, E

Объяснение:

  1. Синхронные вызовы выполняются по порядку: D, A, C, F, H
  2. Все микрозадачи выполняются до задач из Callback Queue: G, B
  3. Задачи из Callback Queue: E

Понимание этих задач демонстрирует глубокое знание работы Event Loop, приоритетов задач и асинхронного программирования в JavaScript.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪