← Собеседование/Как работает событийный цикл (Event Loop)?#367 из 383← ПредыдущийСледующий →+40 XP
Полезно по теме:Маршрут: подготовка к интервьюГайд: портфолио juniorГайд: карьерный планТермин: Closure
← НазадДалее →

Как работает событийный цикл (Event Loop)?

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

JavaScript однопоточный — он выполняет только одну задачу за раз. Event Loop — это механизм, который позволяет JS быть асинхронным: пока выполняется синхронный код, асинхронные операции (таймеры, fetch) обрабатываются браузером/Node.js в фоне, а их колбэки ставятся в очередь. Event Loop следит за тем, чтобы очередь задач выполнялась, когда стек вызовов пуст. Ключевое: микрозадачи (Promise.then) всегда выполняются перед макрозадачами (setTimeout).

Полный разбор

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

Работает по принципу LIFO (Last In, First Out). Каждый вызов функции добавляет фрейм в стек. Когда функция завершается — фрейм удаляется.

function c() { /* ... */ }
function b() { c() }
function a() { b() }
a()

Стек (растёт вниз):
│ c()  │ ← выполняется сейчас
│ b()  │
│ a()  │
│ main │
└──────┘

Когда стек пуст — Event Loop берёт следующую задачу из очереди.

Web APIs / Node.js APIs

Когда JS встречает асинхронную операцию, он передаёт её движку окружения:

setTimeout(fn, 1000)  → Timer API → ждёт 1000ms → кладёт fn в Macrotask Queue
fetch(url)            → Network API → ждёт ответ → кладёт колбэк в Microtask Queue

JS-поток при этом не блокируется — он продолжает выполнять синхронный код.

Две очереди: Microtask и Macrotask

┌─────────────────────────────────────────────────┐
│                   Call Stack                    │
└─────────────────────────────────────────────────┘
         ↑                           ↑
┌────────────────────┐  ┌───────────────────────┐
│  Microtask Queue   │  │   Macrotask Queue     │
│ (приоритет выше)   │  │  (приоритет ниже)     │
│                    │  │                       │
│ Promise.then()     │  │ setTimeout()          │
│ Promise.catch()    │  │ setInterval()         │
│ queueMicrotask()   │  │ I/O callbacks         │
│ MutationObserver   │  │ requestAnimationFrame │
└────────────────────┘  └───────────────────────┘

Алгоритм Event Loop

1. Выполнить весь синхронный код (пока стек не опустеет)
2. Выполнить ВСЕ микрозадачи из Microtask Queue
   (если во время выполнения микрозадачи добавились новые — тоже выполнить)
3. Взять ОДНУ макрозадачу из Macrotask Queue и выполнить её
4. Снова выполнить ВСЕ микрозадачи
5. Повторять с шага 3

Ключевое правило: микрозадачи выполняются все до следующей макрозадачи.

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

console.log('1 — sync')

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

Promise.resolve()
  .then(() => console.log('3 — microtask 1'))
  .then(() => console.log('4 — microtask 2'))

console.log('5 — sync')

// Вывод:
// 1 — sync
// 5 — sync
// 3 — microtask 1
// 4 — microtask 2
// 2 — macrotask

Объяснение:

1. console.log('1') — синхронно

2. setTimeout — колбэк уходит в Macrotask Queue (через 0ms)

3. Promise.resolve().then(...) — колбэк в Microtask Queue

4. console.log('5') — синхронно

5. Стек пуст → выполняем все микрозадачи: 3, затем 4

6. Берём макрозадачу из очереди: 2

Почему Promise быстрее setTimeout(0)?

// Даже если setTimeout задержка = 0мс,
// Promise.then выполнится раньше, потому что
// микрозадачи имеют более высокий приоритет

setTimeout(() => console.log('setTimeout 0ms'), 0)
Promise.resolve().then(() => console.log('Promise.then'))

// Promise.then
// setTimeout 0ms

Опасность бесконечных микрозадач

// ПЛОХО: бесконечный цикл микрозадач заблокирует макрозадачи
function infiniteMicrotasks() {
  Promise.resolve().then(infiniteMicrotasks)
}
// Это заморозит браузер — setTimeout никогда не выполнится!

requestAnimationFrame

Macrotask → Microtasks → rAF → Paint → Macrotask → ...

requestAnimationFrame вызывается перед каждой перерисовкой (~60 раз в секунду). Используй его для анимаций вместо setTimeout.

Связанные уроки курса

  • Event Loop — подробный разбор
  • Промисы
  • async/await
  • Как отвечать на собеседовании

    Начни со структуры: "JavaScript однопоточный. Event Loop — механизм асинхронности. Есть Call Stack, Microtask Queue и Macrotask Queue."

    Объясни приоритеты: "Микрозадачи всегда выполняются перед макрозадачами. Promise.then — микрозадача. setTimeout — макрозадача."

    Покажи пример: запишите console.log + setTimeout(0) + Promise.then и объясни порядок вывода. Это классический вопрос.

    Время ответа: 3-4 минуты. Нарисуй схему с очередями — это очень помогает.

    Красные флаги ответа

    1. "setTimeout(fn, 0) выполняется сразу" — нет. Он попадает в макрозадачу и выполнится только после всех синхронных операций и микрозадач.

    2. Путаница микрозадач и макрозадач — если не знаешь разницу между Promise.then и setTimeout — это провал на большинстве JS-собеседований.

    3. "JavaScript многопоточный" — JS однопоточный. Web Workers — отдельные потоки, но они не имеют доступа к DOM и общаются через сообщения.

    Примеры

    Демонстрация порядка выполнения: синхронный код, микрозадачи (Promise), макрозадачи (setTimeout)

    // Классический вопрос на собеседовании: предскажи порядок вывода
    
    console.log('=== Старт ===')
    
    // Макрозадача (низкий приоритет)
    setTimeout(() => {
      console.log('setTimeout 1 (макрозадача)')
    
      // Микрозадача внутри макрозадачи
      Promise.resolve().then(() => {
        console.log('Promise внутри setTimeout (микрозадача)')
      })
    }, 0)
    
    // Микрозадачи (высокий приоритет)
    Promise.resolve()
      .then(() => {
        console.log('Promise.then #1 (микрозадача)')
        return 'значение'
      })
      .then((val) => {
        console.log('Promise.then #2 (микрозадача), получил:', val)
      })
    
    // Ещё одна микрозадача
    queueMicrotask(() => {
      console.log('queueMicrotask (микрозадача)')
    })
    
    // Ещё одна макрозадача
    setTimeout(() => {
      console.log('setTimeout 2 (макрозадача)')
    }, 0)
    
    console.log('=== Конец синхронного кода ===')
    
    /*
    Ожидаемый вывод:
    === Старт ===
    === Конец синхронного кода ===
    Promise.then #1 (микрозадача)
    queueMicrotask (микрозадача)
    Promise.then #2 (микрозадача)
    setTimeout 1 (макрозадача)
    Promise внутри setTimeout (микрозадача)
    setTimeout 2 (макрозадача)
    
    Почему именно так?
    1. Весь синхронный код выполнился: "Старт" и "Конец"
    2. Все микрозадачи: Promise.then #1, queueMicrotask, Promise.then #2
       (Promise.then #2 добавился в очередь когда выполнился #1)
    3. Первая макрозадача: setTimeout 1
    4. Микрозадачи после макрозадачи: Promise внутри setTimeout
    5. Следующая макрозадача: setTimeout 2
    */
    
    // ===== Визуализация Event Loop =====
    console.log('\n=== Визуализация очередей ===')
    
    function visualEventLoop() {
      const callStack = []
      const microtaskQueue = []
      const macrotaskQueue = []
    
      // Имитация состояния очередей
      function logState(action) {
        console.log(`\n[Action]: ${action}`)
        console.log(`  Call Stack:      [${callStack.join(', ')}]`)
        console.log(`  Microtask Queue: [${microtaskQueue.join(', ')}]`)
        console.log(`  Macrotask Queue: [${macrotaskQueue.join(', ')}]`)
      }
    
      callStack.push('main()')
      macrotaskQueue.push('setTimeout cb')
      microtaskQueue.push('Promise.then')
      logState('После синхронного кода')
    
      callStack.pop()
      logState('Стек пуст — берём микрозадачи')
    
      const micro = microtaskQueue.shift()
      callStack.push(micro)
      logState(`Выполняем микрозадачу: ${micro}`)
    
      callStack.pop()
      logState('Все микрозадачи выполнены')
    
      const macro = macrotaskQueue.shift()
      callStack.push(macro)
      logState(`Берём макрозадачу: ${macro}`)
    
      callStack.pop()
      logState('Цикл завершён')
    }
    
    visualEventLoop()

    Как работает событийный цикл (Event Loop)?

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

    JavaScript однопоточный — он выполняет только одну задачу за раз. Event Loop — это механизм, который позволяет JS быть асинхронным: пока выполняется синхронный код, асинхронные операции (таймеры, fetch) обрабатываются браузером/Node.js в фоне, а их колбэки ставятся в очередь. Event Loop следит за тем, чтобы очередь задач выполнялась, когда стек вызовов пуст. Ключевое: микрозадачи (Promise.then) всегда выполняются перед макрозадачами (setTimeout).

    Полный разбор

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

    Работает по принципу LIFO (Last In, First Out). Каждый вызов функции добавляет фрейм в стек. Когда функция завершается — фрейм удаляется.

    function c() { /* ... */ }
    function b() { c() }
    function a() { b() }
    a()
    
    Стек (растёт вниз):
    │ c()  │ ← выполняется сейчас
    │ b()  │
    │ a()  │
    │ main │
    └──────┘

    Когда стек пуст — Event Loop берёт следующую задачу из очереди.

    Web APIs / Node.js APIs

    Когда JS встречает асинхронную операцию, он передаёт её движку окружения:

    setTimeout(fn, 1000)  → Timer API → ждёт 1000ms → кладёт fn в Macrotask Queue
    fetch(url)            → Network API → ждёт ответ → кладёт колбэк в Microtask Queue

    JS-поток при этом не блокируется — он продолжает выполнять синхронный код.

    Две очереди: Microtask и Macrotask

    ┌─────────────────────────────────────────────────┐
    │                   Call Stack                    │
    └─────────────────────────────────────────────────┘
             ↑                           ↑
    ┌────────────────────┐  ┌───────────────────────┐
    │  Microtask Queue   │  │   Macrotask Queue     │
    │ (приоритет выше)   │  │  (приоритет ниже)     │
    │                    │  │                       │
    │ Promise.then()     │  │ setTimeout()          │
    │ Promise.catch()    │  │ setInterval()         │
    │ queueMicrotask()   │  │ I/O callbacks         │
    │ MutationObserver   │  │ requestAnimationFrame │
    └────────────────────┘  └───────────────────────┘

    Алгоритм Event Loop

    1. Выполнить весь синхронный код (пока стек не опустеет)
    2. Выполнить ВСЕ микрозадачи из Microtask Queue
       (если во время выполнения микрозадачи добавились новые — тоже выполнить)
    3. Взять ОДНУ макрозадачу из Macrotask Queue и выполнить её
    4. Снова выполнить ВСЕ микрозадачи
    5. Повторять с шага 3

    Ключевое правило: микрозадачи выполняются все до следующей макрозадачи.

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

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

    Объяснение:

    1. console.log('1') — синхронно

    2. setTimeout — колбэк уходит в Macrotask Queue (через 0ms)

    3. Promise.resolve().then(...) — колбэк в Microtask Queue

    4. console.log('5') — синхронно

    5. Стек пуст → выполняем все микрозадачи: 3, затем 4

    6. Берём макрозадачу из очереди: 2

    Почему Promise быстрее setTimeout(0)?

    // Даже если setTimeout задержка = 0мс,
    // Promise.then выполнится раньше, потому что
    // микрозадачи имеют более высокий приоритет
    
    setTimeout(() => console.log('setTimeout 0ms'), 0)
    Promise.resolve().then(() => console.log('Promise.then'))
    
    // Promise.then
    // setTimeout 0ms

    Опасность бесконечных микрозадач

    // ПЛОХО: бесконечный цикл микрозадач заблокирует макрозадачи
    function infiniteMicrotasks() {
      Promise.resolve().then(infiniteMicrotasks)
    }
    // Это заморозит браузер — setTimeout никогда не выполнится!

    requestAnimationFrame

    Macrotask → Microtasks → rAF → Paint → Macrotask → ...

    requestAnimationFrame вызывается перед каждой перерисовкой (~60 раз в секунду). Используй его для анимаций вместо setTimeout.

    Связанные уроки курса

  • Event Loop — подробный разбор
  • Промисы
  • async/await
  • Как отвечать на собеседовании

    Начни со структуры: "JavaScript однопоточный. Event Loop — механизм асинхронности. Есть Call Stack, Microtask Queue и Macrotask Queue."

    Объясни приоритеты: "Микрозадачи всегда выполняются перед макрозадачами. Promise.then — микрозадача. setTimeout — макрозадача."

    Покажи пример: запишите console.log + setTimeout(0) + Promise.then и объясни порядок вывода. Это классический вопрос.

    Время ответа: 3-4 минуты. Нарисуй схему с очередями — это очень помогает.

    Красные флаги ответа

    1. "setTimeout(fn, 0) выполняется сразу" — нет. Он попадает в макрозадачу и выполнится только после всех синхронных операций и микрозадач.

    2. Путаница микрозадач и макрозадач — если не знаешь разницу между Promise.then и setTimeout — это провал на большинстве JS-собеседований.

    3. "JavaScript многопоточный" — JS однопоточный. Web Workers — отдельные потоки, но они не имеют доступа к DOM и общаются через сообщения.

    Примеры

    Демонстрация порядка выполнения: синхронный код, микрозадачи (Promise), макрозадачи (setTimeout)

    // Классический вопрос на собеседовании: предскажи порядок вывода
    
    console.log('=== Старт ===')
    
    // Макрозадача (низкий приоритет)
    setTimeout(() => {
      console.log('setTimeout 1 (макрозадача)')
    
      // Микрозадача внутри макрозадачи
      Promise.resolve().then(() => {
        console.log('Promise внутри setTimeout (микрозадача)')
      })
    }, 0)
    
    // Микрозадачи (высокий приоритет)
    Promise.resolve()
      .then(() => {
        console.log('Promise.then #1 (микрозадача)')
        return 'значение'
      })
      .then((val) => {
        console.log('Promise.then #2 (микрозадача), получил:', val)
      })
    
    // Ещё одна микрозадача
    queueMicrotask(() => {
      console.log('queueMicrotask (микрозадача)')
    })
    
    // Ещё одна макрозадача
    setTimeout(() => {
      console.log('setTimeout 2 (макрозадача)')
    }, 0)
    
    console.log('=== Конец синхронного кода ===')
    
    /*
    Ожидаемый вывод:
    === Старт ===
    === Конец синхронного кода ===
    Promise.then #1 (микрозадача)
    queueMicrotask (микрозадача)
    Promise.then #2 (микрозадача)
    setTimeout 1 (макрозадача)
    Promise внутри setTimeout (микрозадача)
    setTimeout 2 (макрозадача)
    
    Почему именно так?
    1. Весь синхронный код выполнился: "Старт" и "Конец"
    2. Все микрозадачи: Promise.then #1, queueMicrotask, Promise.then #2
       (Promise.then #2 добавился в очередь когда выполнился #1)
    3. Первая макрозадача: setTimeout 1
    4. Микрозадачи после макрозадачи: Promise внутри setTimeout
    5. Следующая макрозадача: setTimeout 2
    */
    
    // ===== Визуализация Event Loop =====
    console.log('\n=== Визуализация очередей ===')
    
    function visualEventLoop() {
      const callStack = []
      const microtaskQueue = []
      const macrotaskQueue = []
    
      // Имитация состояния очередей
      function logState(action) {
        console.log(`\n[Action]: ${action}`)
        console.log(`  Call Stack:      [${callStack.join(', ')}]`)
        console.log(`  Microtask Queue: [${microtaskQueue.join(', ')}]`)
        console.log(`  Macrotask Queue: [${macrotaskQueue.join(', ')}]`)
      }
    
      callStack.push('main()')
      macrotaskQueue.push('setTimeout cb')
      microtaskQueue.push('Promise.then')
      logState('После синхронного кода')
    
      callStack.pop()
      logState('Стек пуст — берём микрозадачи')
    
      const micro = microtaskQueue.shift()
      callStack.push(micro)
      logState(`Выполняем микрозадачу: ${micro}`)
    
      callStack.pop()
      logState('Все микрозадачи выполнены')
    
      const macro = macrotaskQueue.shift()
      callStack.push(macro)
      logState(`Берём макрозадачу: ${macro}`)
    
      callStack.pop()
      logState('Цикл завершён')
    }
    
    visualEventLoop()

    Задание

    Предскажи и объясни порядок вывода console.log для следующего кода. Реализуй функцию predictOutput(steps), которая принимает массив шагов с типами (sync/microtask/macrotask) и возвращает их в правильном порядке выполнения.

    Подсказка

    Порядок вывода ACGEBFD. Функция predictOutput: [...sync, ...microtasks, ...macrotasks].map(s => s.label). Помни: C добавляет новую макрозадачу D, поэтому D идёт после F.

    Загружаем среду выполнения...
    Загружаем AI-помощника...