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

Какой механизм позволяет делать асинхронные операции в JavaScript?

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

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

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

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

Механизм асинхронных операций в JavaScript

Asynchronous JavaScript полагается на несколько механизмов: Event Loop, Callback Queue, Promises, async/await. Это сложная система, которая позволяет выполнять долгие операции без блокировки интерфейса.

1. Call Stack и Event Loop

JavaScript работает в один поток (single-threaded). Event Loop управляет выполнением кода:

console.log('1. Start');

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

console.log('3. End');

// Порядок вывода:
// 1. Start
// 3. End
// 2. Timeout callback

// Даже с timeout = 0, callback выполнится ПОСЛЕ синхронного кода

Почему так? Потому что setTimeout добавляет callback в Callback Queue, а Event Loop сначала выполняет весь Call Stack.

Call Stack: [console.log('Start'), setTimeout, console.log('End')]
Callback Queue: [console.log('Timeout callback')]

Event Loop ждёт пока Call Stack пуст, потом берёт из Queue

2. Callbacks

Первый способ работать с асинхронностью — использовать callbacks:

function loadUser(id, callback) {
  setTimeout(() => {
    const user = { id, name: 'John' };
    callback(user);
  }, 1000);
}

loadUser(1, (user) => {
  console.log('User loaded:', user);
  
  // "Callback Hell" — вложенные callback'и
  loadUser(user.id + 1, (user2) => {
    console.log('User 2:', user2);
    
    loadUser(user2.id + 1, (user3) => {
      console.log('User 3:', user3);
      // Код становится нечитаемым
    });
  });
});

Проблемы:

  • Сложная читаемость (Callback Hell)
  • Трудно обрабатывать ошибки
  • Трудно управлять потоком выполнения

3. Promises

Промисы решили проблему callback'ов и предоставляют лучший способ работать с асинхронностью:

function loadUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: 'John' });
      } else {
        reject(new Error('Invalid ID'));
      }
    }, 1000);
  });
}

// Использование
loadUser(1)
  .then(user => {
    console.log('User:', user);
    return loadUser(user.id + 1);
  })
  .then(user2 => {
    console.log('User 2:', user2);
    return loadUser(user2.id + 1);
  })
  .then(user3 => console.log('User 3:', user3))
  .catch(error => console.error('Error:', error));

Promise states:

const promise = new Promise((resolve, reject) => {
  // pending — начальное состояние
  
  setTimeout(() => {
    resolve('Success'); // fulfilled — успешно
    // или
    reject(new Error('Failed')); // rejected — ошибка
  }, 1000);
});

// После resolve/reject состояние не изменяется
promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('Done'));

Microtask Queue

Есть два типа очередей: Callback Queue и Microtask Queue. Promises используют Microtask Queue, которая приоритетнее:

console.log('1. Start');

setTimeout(() => {
  console.log('5. setTimeout callback'); // Callback Queue
}, 0);

Promise.resolve()
  .then(() => console.log('2. Promise 1')) // Microtask Queue
  .then(() => console.log('3. Promise 2')); // Microtask Queue

console.log('4. End');

// Порядок:
// 1. Start
// 4. End
// 2. Promise 1 (Microtask очищается первой)
// 3. Promise 2
// 5. setTimeout callback (Callback Queue очищается после)

4. Async/Await

Это синтаксический сахар над Promises, который делает асинхронный код похожим на синхронный:

async function loadUsers() {
  try {
    const user1 = await loadUser(1);
    console.log('User 1:', user1);
    
    const user2 = await loadUser(user1.id + 1);
    console.log('User 2:', user2);
    
    const user3 = await loadUser(user2.id + 1);
    console.log('User 3:', user3);
    
    return [user1, user2, user3];
  } catch (error) {
    console.error('Error:', error);
  }
}

// async функция всегда возвращает Promise
loadUsers().then(users => console.log('All users:', users));

5. Параллельные операции

Последовательное выполнение (один за другим)

async function sequential() {
  const user1 = await loadUser(1); // 1000ms
  const user2 = await loadUser(2); // 1000ms
  const user3 = await loadUser(3); // 1000ms
  // Итого: ~3000ms
}

Параллельное выполнение (одновременно)

async function parallel() {
  // Promise.all завершается когда все промисы выполнены
  // или когда первый отклонён
  const [user1, user2, user3] = await Promise.all([
    loadUser(1),
    loadUser(2),
    loadUser(3)
  ]);
  // Итого: ~1000ms
}

// Promise.allSettled завершается когда все промисы settled
// (не отклоняет при ошибке)
const results = await Promise.allSettled([
  loadUser(1),
  loadUser(2),
  loadUser('invalid')
]);

console.log(results); // [
  // { status: 'fulfilled', value: {...} },
  // { status: 'fulfilled', value: {...} },
  // { status: 'rejected', reason: Error }
// ]

6. Web APIs

Внешние API (setTimeout, fetch, XMLHttpRequest) делегируют работу браузеру:

console.log('1. Start');

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log('3. Data:', data));

console.log('2. End');

// Поток:
// 1. Call Stack: выполняется синхронно
// 2. fetch вызывает Web API
// 3. Web API в фоне делает запрос
// 4. Когда ответ придёт, Promise callback идёт в Microtask Queue
// 5. Event Loop выполняет callback

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

class DataFetcher {
  private cache = new Map();
  
  async fetchUser(id: number) {
    if (this.cache.has(id)) {
      return this.cache.get(id);
    }
    
    try {
      const response = await fetch(`/api/users/${id}`);
      if (!response.ok) throw new Error('Not found');
      
      const data = await response.json();
      this.cache.set(id, data);
      return data;
    } catch (error) {
      console.error('Failed to fetch user:', error);
      throw error;
    }
  }
  
  async fetchUsers(ids: number[]) {
    // Параллельная загрузка
    return Promise.all(ids.map(id => this.fetchUser(id)));
  }
  
  async fetchUserWithDetails(id: number) {
    // Последовательная загрузка
    const user = await this.fetchUser(id);
    const posts = await fetch(`/api/users/${id}/posts`)
      .then(r => r.json());
    
    return { ...user, posts };
  }
}

// Использование
const fetcher = new DataFetcher();

// Параллельная загрузка нескольких пользователей
const users = await fetcher.fetchUsers([1, 2, 3]);

// С обработкой ошибок
try {
  const userWithDetails = await fetcher.fetchUserWithDetails(1);
  console.log(userWithDetails);
} catch (error) {
  console.error('Error:', error);
}

8. Обработка ошибок

// Promise catch
fetch('/api/data')
  .then(r => r.json())
  .catch(error => console.error('Error:', error))
  .finally(() => console.log('Request complete'));

// Async/await try-catch
async function loadData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error; // пробросить дальше если нужно
  } finally {
    console.log('Request complete');
  }
}

Временная шкала выполнения

console.log('Script start');

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

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

fetch('/api/data')
  .then(() => console.log('fetch response'));

console.log('Script end');

// Вывод:
// Script start
// Script end
// Promise 1 (Microtask Queue)
// Promise 2 (Microtask Queue)
// fetch response (Microtask Queue)
// setTimeout 1 (Callback Queue)

Вывод

Asynchronous JavaScript опирается на:

  1. Event Loop — управляет выполнением
  2. Call Stack — синхронный код
  3. Web APIs — делегирование браузеру
  4. Microtask Queue — Promises (приоритетнее)
  5. Callback Queue — setTimeout, события

МодернВ современном коде используйте async/await вместо callbacks и Promises, так как это более читаемо и удобнее обрабатывать ошибки.