Gernar
JavaScript: асинхронность

Как устроена асинхронность в JavaScript

Разбор вопроса «Как устроена асинхронность в JavaScript» для Frontend Developer: что проверяет интервьюер, ключевые тезисы, практические примеры и частые ошибки.

Вопрос

Как устроена асинхронность в JavaScript

Профессия

Frontend Developer

Что хочет услышать интервьюер

Интервьюер хочет убедиться, что кандидат понимает механизм Event Loop, разницу между макрозадачами и микрозадачами, а также умеет работать с асинхронным кодом, используя промисы и async/await.

Ключевые тезисы

  • JavaScript использует однопоточную модель выполнения, но поддерживает асинхронность через Event Loop.
  • Асинхронные операции, такие как setTimeout, fetch или Promise, обрабатываются через очередь задач (task queue) и микрозадач (microtask queue).
  • Event Loop обеспечивает выполнение кода, проверяя состояние стека вызовов и очередей задач.
  • Промисы (Promises) и async/await предоставляют удобный синтаксис для работы с асинхронным кодом.
  • Микрозадачи (microtasks) выполняются перед макрозадачами (macrotasks), что важно для понимания порядка выполнения.

Подробный ответ

JavaScript — однопоточный язык, но он поддерживает асинхронность благодаря механизму Event Loop. Это означает, что код выполняется последовательно в одном потоке, но долгие операции (например, запросы к серверу или таймеры) не блокируют выполнение основного потока. Вместо этого они передаются в очередь задач (task queue) или микрозадач (microtask queue), а Event Loop управляет их выполнением после завершения синхронного кода.

Event Loop работает циклически: он проверяет, пуст ли стек вызовов (call stack). Если стек пуст, Event Loop берет первую задачу из очереди микрозадач (если она не пуста) и выполняет ее. Микрозадачи включают обработку промисов (Promise) и MutationObserver. Если очередь микрозадач пуста, Event Loop переходит к очереди макрозадач (например, setTimeout, setInterval, I/O операции) и выполняет следующую задачу оттуда.

Промисы (Promises) и async/await — это синтаксические надстройки для работы с асинхронным кодом. Промисы позволяют избежать «ада колбэков» (callback hell), а async/await делает код еще более читаемым, позволяя писать асинхронный код в стиле, похожем на синхронный. Под капотом async/await использует промисы: функция с async всегда возвращает промис, а await «приостанавливает» выполнение функции до разрешения промиса.

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

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

Пример 1

Пример 1: Порядок выполнения микрозадач и макрозадач

console.log('Start');

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

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

console.log('End');

// Результат:
// Start
// End
// Promise
// Timeout

Здесь сначала выполняется синхронный код (Start, End), затем микрозадача (Promise), и только потом макрозадача (Timeout).

Пример 2

Пример 2: Вложенные микрозадачи

Promise.resolve().then(() => {
  console.log('Promise 1');
  Promise.resolve().then(() => console.log('Promise 2'));
});

// Результат:
// Promise 1
// Promise 2

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

Пример 3

Пример 3: Обработка ошибок в асинхронном коде

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

fetchData();

Использование try/catch с async/await позволяет обрабатывать ошибки так же, как в синхронном коде.

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

  • Непонимание порядка выполнения микрозадач и макрозадач. Например, ожидание, что setTimeout с нулевой задержкой выполнится сразу после синхронного кода, хотя на самом деле он попадет в очередь макрозадач.
  • Пренебрежение обработкой ошибок в асинхронном коде, особенно при использовании промисов без .catch() или async/await без try/catch.
  • Создание бесконечных циклов микрозадач (например, рекурсивное создание промисов), что может привести к зависанию приложения.

Связанные темы

  • Promise и его методы (then, catch, finally, all, race, allSettled)
  • Работа с Web APIs (setTimeout, fetch, DOM events)
  • Генераторы (Generators) и их использование с асинхронным кодом
  • Работа с Web Workers для многопоточности

Follow-up вопросы

Что такое Event Loop и как он работает?

Уровень: basic

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

В чем разница между микрозадачами (microtasks) и макрозадачами (macrotasks)?

Уровень: intermediate

Микрозадачи выполняются сразу после текущего синхронного кода и перед макрозадачами. Пример микрозадачи — обработка промисов. Макрозадачи, такие как setTimeout, выполняются после микрозадач.

Как работает async/await под капотом?

Уровень: intermediate

async/await — это синтаксический сахар над промисами. Ключевое слово async возвращает промис, а await приостанавливает выполнение функции до разрешения промиса.

Что произойдет, если внутри микрозадачи создать другую микрозадачу?

Уровень: advanced

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

Как обрабатываются ошибки в асинхронном коде?

Уровень: basic

Ошибки в асинхронном коде можно обрабатывать через try/catch в сочетании с async/await или через метод .catch() для промисов. Это позволяет избежать незахваченных исключений.

Содержание