← Назад к вопросам

Как выполняется асинхронный код?

2.0 Middle🔥 261 комментариев
#JavaScript Core#Браузер и сетевые технологии

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как выполняется асинхронный код в JavaScript

JavaScript работает по принципу event loop — это механизм, который координирует выполнение синхронного кода, асинхронных операций и обработку событий. Это критично понимать для написания правильного асинхронного кода.

JavaScript: однопоточный язык

JavaScript имеет один поток выполнения (главный поток), поэтому ему нужен механизм для выполнения асинхронных операций без блокировки:

console.log('1');

setTimeout(() => {
  console.log('2');
}, 1000);

console.log('3');

// Вывод:
// 1
// 3
// 2 (через 1 секунду)

Это работает потому что setTimeout не блокирует выполнение кода.

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

Все синхронные операции выполняются в call stack:

function a() {
  b();
}

function b() {
  c();
}

function c() {
  console.log('c');
}

a();

// Стек вызовов:
// 1. a() — добавляется в стек
// 2. b() — добавляется в стек (внутри a)
// 3. c() — добавляется в стек (внутри b)
// 4. console.log — выполняется
// 5. c() выходит из стека
// 6. b() выходит из стека
// 7. a() выходит из стека

Event Loop

Event Loop — это цикл, который проверяет стек вызовов и очередь callbacks:

console.log('Начало');

setTimeout(() => {
  console.log('setTimeout');
}, 0); // Даже с 0 ms выполнится позже!

console.log('Конец');

// Вывод:
// Начало
// Конец
// setTimeout

Почему так? Даже setTimeout с 0ms отправляет callback в очередь, и он выполняется только когда call stack пуст.

Web APIs и очереди

В браузере есть несколько очередей для асинхронных операций:

1. Macrotask Queue (очередь макротасков)

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • requestAnimationFrame
  • I/O операции
setTimeout(() => {
  console.log('Макротаск');
}, 0);

2. Microtask Queue (очередь микротасков) — выше приоритет!

  • Promise (.then, .catch, .finally)
  • async/await
  • MutationObserver
  • queueMicrotask
Promise.resolve().then(() => {
  console.log('Микротаск');
});

Порядок выполнения

console.log('1. Синхронный код');

setTimeout(() => {
  console.log('2. setTimeout (макротаск)');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise.then (микротаск)');
});

console.log('4. Ещё синхронный код');

// Вывод:
// 1. Синхронный код
// 4. Ещё синхронный код
// 3. Promise.then (микротаск)
// 2. setTimeout (макротаск)

Event Loop в деталях

  1. Выполнить весь синхронный код из call stack
  2. Выполнить ВСЕ микротаски (Promises, queueMicrotask)
  3. Выполнить один макротаск (setTimeout, setInterval)
  4. Проверить UI-обновления (repaint, reflow)
  5. Повторить с шага 2
console.log('Старт');

setTimeout(() => {
  console.log('setTimeout 1');
  Promise.resolve().then(() => console.log('Promise in setTimeout'));
}, 0);

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

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

console.log('Конец');

// Вывод:
// Старт
// Конец
// Promise 1
// Promise 2
// setTimeout 1
// Promise in setTimeout
// setTimeout 2
// setTimeout in Promise

Async/Await

Async/await работает с Promises и выполняется в microtask queue:

async function example() {
  console.log('1. async функция начала');
  
  await Promise.resolve();
  console.log('2. После await (микротаск)');
}

console.log('0. Начало');
example();
console.log('3. После вызова async');

// Вывод:
// 0. Начало
// 1. async функция начала
// 3. После вызова async
// 2. После await (микротаск)

Fetch API и Promises

Fetch возвращает Promise, поэтому использует microtask queue:

console.log('Старт');

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log('Данные:', data));

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

console.log('Конец');

// Вывод:
// Старт
// Конец
// Данные: ... (когда fetch завершится)
// setTimeout

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

let result = null;

fetch('/api/data')
  .then(response => response.json())
  .then(data => {
    result = data; // Микротаск — выполнится быстро
  });

setTimeout(() => {
  console.log(result); // Макротаск — выполнится позже
}, 1000);

// result будет заполнен в 999ms, выведется в 1000ms

requestAnimationFrame

Это специальный макротаск, выполняется перед перерисовкой экрана:

console.log('Старт');

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
});

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

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

console.log('Конец');

// Вывод:
// Старт
// Конец
// Promise
// requestAnimationFrame
// setTimeout

Как отладить асинхронный код

// 1. Используй async/await (более понятно)
async function main() {
  try {
    const user = await fetchUser();
    const posts = await fetchPosts(user.id);
    console.log(posts);
  } catch (error) {
    console.error('Ошибка:', error);
  }
}

// 2. Добавляй console.log с временем
console.time('loading');
await fetchData();
console.timeEnd('loading');

// 3. Используй DevTools для профилирования
// Chrome DevTools → Performance → Record

Ключевые моменты

  • JavaScript однопоточный, но Web APIs позволяют работать асинхронно
  • Event Loop управляет порядком выполнения кода
  • Микротаски (Promises) выполняются раньше макротасков (setTimeout)
  • Call stack должен быть пуст перед выполнением callbacks
  • Async/await — синтаксический сахар над Promises
  • requestAnimationFrame выполняется перед перерисовкой экрана

Понимание event loop критично для написания эффективного асинхронного JavaScript кода и предотвращения race conditions.

Как выполняется асинхронный код? | PrepBro