Gernar
Frontend DeveloperJavaScript, асинхронность

Интервью-вопрос

Что такое Event Loop?

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

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 60 правильно

Как лучше объяснить порядок Promise и setTimeout?

Вы видите код с Promise.resolve().then(...) и setTimeout(..., 0) после синхронных console.log.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

JavaScript выполняет ваш код в Call Stack. Пока стек занят, новая работа не стартует. Поэтому обычный синхронный код всегда идет первым, даже если рядом уже запланированы таймеры, промисы или обработчики событий.

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

Короткая формулировка для интервью:

Event Loop следит за тем, когда Call Stack свободен, и переносит в выполнение запланированные задачи. При этом микротаски после текущего кода выполняются раньше следующей обычной задачи.

Порядок выполнения в браузере

Для большинства frontend-задач полезна такая модель:

  1. Выполняется текущий синхронный код в Call Stack.
  2. После завершения текущей задачи очищается очередь микротасок.
  3. Если нужно и есть возможность, браузер может обновить rendering.
  4. Event Loop берет следующую задачу, например таймер, пользовательское событие или продолжение сетевой операции.
  5. Цикл повторяется.

Важно не превращать это в слишком грубое правило. Браузерная спецификация сложнее, а rendering зависит от состояния страницы и кадра. Но для ответа на интервью достаточно показать главный принцип: микротаски идут до следующей задачи, а основной поток должен освобождаться.

Как разбирать порядок выполнения

1Есть обычный синхронный код?
Выполните его первым, пока стек вызовов не станет пустым.
2Есть then, catch, finally, await continuation или queueMicrotask?
Поставьте их в микротаски и выполните до таймеров.
3Есть setTimeout, события или другие задачи?
Берите их после микротасок, по одной задаче за проход цикла.
4Код долго занимает основной поток?
Ожидайте задержку UI, rendering, таймеров и пользовательских событий.

Пример с Promise и setTimeout

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

console.log("start");

setTimeout(() => {
  console.log("timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("promise");
});

console.log("end");

Вывод будет таким:

start
end
promise
timeout

start и end выводятся синхронно. Колбэк then попадает в микротаски. Колбэк setTimeout попадает в очередь задач. После освобождения стека сначала выполняется микротаска, и только потом таймер.

Асинхронность и состояние UI

Event Loop помогает понять не только порядок логов. Он также объясняет, почему ответ сети может прийти позже, чем пользователь уже изменил экран. В React из-за этого часто появляется устаревший UI.

Плохой пример:

useEffect(() => {
  setLoading(true);

  fetch(`/api/search?q=${query}`)
    .then((response) => response.json())
    .then((data) => {
      setItems(data);
      setLoading(false);
    })
    .catch(() => {
      setLoading(false);
    });
}, [query]);

Что сломается: медленный ответ на старый query может перезаписать результаты для нового запроса. Еще loading может стать false для неправильного запроса. Ошибка сервера не попадет в понятный error state, а текст с амперсандом может сломать параметры URL.

Безопаснее отменять запрос и не применять результат, если эффект уже не актуален:

useEffect(() => {
  const controller = new AbortController();
  let cancelled = false;

  setLoading(true);
  setError(null);

  fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: controller.signal,
  })
    .then((response) => {
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return response.json();
    })
    .then((data) => {
      if (!cancelled) {
        setItems(data);
      }
    })
    .catch((error) => {
      if (!cancelled && error.name !== "AbortError") {
        setError(error);
      }
    })
    .finally(() => {
      if (!cancelled) {
        setLoading(false);
      }
    });

  return () => {
    cancelled = true;
    controller.abort();
  };
}, [query]);

Здесь AbortController убирает лишнюю сетевую работу, cancelled защищает состояние компонента, а encodeURIComponent не дает пользовательскому тексту сломать URL.

Что блокирует Event Loop

Плохой пример:

button.addEventListener("click", () => {
  const start = Date.now();

  while (Date.now() - start < 5000) {
    // имитация тяжелой синхронной работы
  }

  console.log("done");
});

Этот обработчик занимает основной поток примерно на 5 секунд. За это время браузер не сможет нормально обработать новые клики, обновить анимацию или быстро отрисовать изменения. Event Loop не "перепрыгнет" через текущий стек, потому что сначала должен завершиться уже выполняемый код.

Безопаснее разбить тяжелую работу на части, вынести ее в Web Worker или хотя бы планировать куски работы так, чтобы браузер получал паузы для UI. Если задача связана с React, не стоит маскировать тяжелые вычисления одним setTimeout. Нужно уменьшать саму работу, мемоизировать дорогие вычисления по делу или переносить их из главного потока.

Безопаснее для UI
  1. 1Выполнить короткий синхронный код
  2. 2Дождаться микротасок после текущей операции
  3. 3Дать браузеру шанс обработать события и rendering
  4. 4Разбить тяжелую работу на части
Опасно для UI
  1. 1Запустить долгий цикл в основном потоке
  2. 2Создавать бесконечную цепочку микротасок
  3. 3Ожидать, что таймер спасет уже заблокированный стек
  4. 4Получить фриз интерфейса и задержку кликов

Микротаски и rendering

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

Плохой пример:

function loop() {
  queueMicrotask(loop);
}

loop();

Такой код может надолго занять очередь микротасок. В реальном интерфейсе это выглядит как зависание, хотя формально вы используете "асинхронный" API. На интервью лучше прямо сказать, что асинхронность не равна безопасности для UI.

Браузер и Node.js

В браузере вы обычно говорите про Call Stack, Web APIs, очередь задач, микротаски и rendering. Этого достаточно для frontend-разработчика, если вопрос не уходит в серверный JavaScript.

В Node.js принцип Event Loop похож, но реализация другая. Там есть фазы libuv, например timers, poll, check и close callbacks. Также есть process.nextTick, который не стоит автоматически переносить в браузерную модель. Сильная формулировка звучит так: общий принцип один, но детали планирования зависят от среды выполнения.

Как отвечать на интервью

Держите ответ коротким и проверяемым. Сначала дайте модель, затем порядок очередей, затем практический риск.

Пример ответа:

Event Loop это механизм, который позволяет JavaScript обрабатывать асинхронные события в одном основном потоке. Синхронный код выполняется в Call Stack. Когда стек пустеет, сначала выполняются микротаски, например обработчики Promise, затем берутся обычные задачи вроде таймеров и событий. Поэтому setTimeout с задержкой 0 не выполняется сразу, а тяжелый синхронный код блокирует интерфейс и отрисовку.

Если хотите усилить ответ, добавьте пример с Promise.then и setTimeout. Не уходите сразу в детали всех фаз Node.js, если вас спрашивают про браузер и frontend.

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Считать setTimeout с 0 мгновенным

    setTimeout(fn, 0) не запускает fn прямо сейчас. Он ставит колбэк в очередь задач, поэтому перед ним завершится текущий код и микротаски. Без этого понимания легко ошибиться в порядке вывода и в логике UI.
  2. 2

    Забывать про приоритет микротасок

    Обработчики Promise.then, продолжение после await и queueMicrotask выполняются раньше следующей задачи. Если сказать только про одну очередь callback queue, ответ будет неполным. На практике это влияет на состояние компонента, порядок логов и момент обновления UI.
  3. 3

    Думать, что асинхронность убирает блокировку

    Асинхронный API не спасает код, который уже занял основной поток. Долгий цикл, тяжелый JSON parse или синхронные вычисления все равно задержат клики и paint. Безопаснее разбивать работу, переносить ее в Web Worker или выполнять порциями.
  4. 4

    Не защищать UI от устаревших асинхронных результатов

    Event Loop выполнит продолжение промиса, когда до него дойдет очередь, но он не знает, какой запрос для интерфейса уже устарел. В поиске, автокомплите или фильтрах старый ответ может перезаписать новое состояние. Нужны отмена через AbortController, проверка актуальности или другая защита от race condition.
  5. 5

    Смешивать браузерную модель и Node.js фазы

    Для frontend-ответа достаточно очередей задач, микротасок, стека и rendering. Если вы начинаете подробно говорить про setImmediate и фазы libuv, уточните, что это Node.js. Иначе ответ звучит так, будто вы переносите детали одной среды в другую.
  6. 6

    Игнорировать rendering

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

Follow-up

Что могут спросить дальше

Короткие ответы помогут закрепить очереди, микротаски и блокировку UI.

Живые ответы

Видео с похожим вопросом

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

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

Содержание