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

Как можно организовать ассинхронность браузера?

2.3 Middle🔥 141 комментариев
#JavaScript Core

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

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

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

Почему асинхронность важна

JavaScript в браузере однопоточный (single-threaded), но может выполнять асинхронные операции. Без асинхронности браузер зависал бы при каждом запросе к серверу или длительной операции. Асинхронность позволяет приложению оставаться отзывчивым.

Архитектура браузера: Event Loop

Браузер использует Event Loop архитектуру для управления асинхронностью:

┌─────────────────────────────────────┐
│   Основной поток (Main Thread)      │
│  ┌─────────────────────────────────┐│
│  │   Call Stack (стек вызовов)     ││
│  │  - Выполняет JavaScript код     ││
│  └─────────────────────────────────┘│
│                                     │
│  ┌─────────────────────────────────┐│
│  │   Event Queue (очередь событий) ││
│  │  - setTimeout callbacks          ││
│  │  - Promise resolutions           ││
│  │  - fetch responses               ││
│  └─────────────────────────────────┘│
│                                     │
│  ┌─────────────────────────────────┐│
│  │   Event Loop (цикл событий)     ││
│  │  - Переносит события из queue   ││
│  │    в call stack когда он пуст  ││
│  └─────────────────────────────────┘│
└─────────────────────────────────────┘
     ↓
┌─────────────────────────────────────┐
│   Web APIs (Browser APIs)           │
│  - setTimeout/setInterval           │
│  - fetch/XHR                        │
│  - DOM events                       │
│  - Worker threads                   │
│  - IndexedDB                        │
└─────────────────────────────────────┘

Способы организации асинхронности

1. Callbacks (Обратные вызовы)

Самый простой способ - передать функцию, которая выполнится позже:

function fetchUserData(userId, callback) {
  // Имитация сетевого запроса
  setTimeout(() => {
    const user = { id: userId, name: 'Иван' };
    callback(user); // Вызываем callback
  }, 1000);
}

// Использование
fetchUserData(1, (user) => {
  console.log('Получены данные:', user);
});

console.log('Запрос отправлен'); // Выполнится сразу

Проблема - Callback Hell:

// ❌ Плохо: вложенные callbacks
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      console.log('Все данные загружены');
    });
  });
});

2. Promises (Обещания)

В отличие от callbacks, Promises дают лучший контроль и читаемость:

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: 'Мария' });
      } else {
        reject(new Error('Некорректный ID'));
      }
    }, 1000);
  });
}

// Использование
fetchUserData(1)
  .then(user => {
    console.log('Пользователь:', user);
    return fetchPosts(user.id);
  })
  .then(posts => {
    console.log('Посты:', posts);
    return fetchComments(posts[0].id);
  })
  .then(comments => console.log('Комментарии:', comments))
  .catch(error => console.error('Ошибка:', error))
  .finally(() => console.log('Готово'));

Методы работы с несколькими Promises:

// Promise.all - все должны успешно выполниться
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts()
]);

// Promise.race - первый результат побеждает
const fastest = await Promise.race([
  fetchFromServer1(),
  fetchFromServer2()
]);

// Promise.allSettled - все статусы (не выбросит ошибку)
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Успех:', result.value);
  } else {
    console.log('Ошибка:', result.reason);
  }
});

3. Async/Await (Асинхронные функции)

Современный и наиболее читаемый способ:

async function loadAllData() {
  try {
    // Код выглядит как синхронный, но на самом деле асинхронный
    const user = await fetchUser(1);
    console.log('Пользователь:', user);
    
    const posts = await fetchPosts(user.id);
    console.log('Посты:', posts);
    
    const comments = await fetchComments(posts[0].id);
    console.log('Комментарии:', comments);
    
    return { user, posts, comments };
  } catch (error) {
    console.error('Ошибка:', error);
  } finally {
    console.log('Загрузка завершена');
  }
}

// Вызов
await loadAllData();

Параллельное выполнение:

// Последовательно (медленно)
async function sequential() {
  const user = await fetchUser(1);    // 1 сек
  const posts = await fetchPosts(1);  // 1 сек
  return { user, posts };             // Всего 2 сек
}

// Параллельно (быстро)
async function parallel() {
  const [user, posts] = await Promise.all([
    fetchUser(1),   // 1 сек
    fetchPosts(1)   // 1 сек (одновременно)
  ]);               // Всего 1 сек
  return { user, posts };
}

4. Event Listeners (Слушатели событий)

Для обработки событий от пользователя и браузера:

// Клик мышки
button.addEventListener('click', () => {
  console.log('Кнопка нажата');
});

// Изменение input
input.addEventListener('change', (event) => {
  console.log('Новое значение:', event.target.value);
});

// Скролл страницы
window.addEventListener('scroll', () => {
  console.log('Прокрутка:', window.scrollY);
});

// Загрузка страницы
window.addEventListener('load', () => {
  console.log('Страница полностью загружена');
});

5. setTimeout / setInterval

Для отложенного выполнения кода:

// Выполнить один раз через 2 секунды
const timerId = setTimeout(() => {
  console.log('Выполнилось через 2 сек');
}, 2000);

// Отменить если нужно
clearTimeout(timerId);

// Выполнять каждую секунду
const intervalId = setInterval(() => {
  console.log('Выполняется каждую секунду');
}, 1000);

// Остановить
clearInterval(intervalId);

// requestAnimationFrame - оптимально для анимаций
let frameId;
function animate() {
  // Код анимации
  frameId = requestAnimationFrame(animate); // Повторить в следующем кадре
}
animate();

// Остановить анимацию
cancelAnimationFrame(frameId);

6. Web Workers (Многопоточность)

Для тяжёлых вычислений в отдельном потоке:

// main.js
const worker = new Worker('worker.js');

// Отправляем данные в worker
worker.postMessage({ task: 'calculate', data: [1, 2, 3] });

// Получаем результат
worker.onmessage = (event) => {
  console.log('Результат:', event.data);
};
// worker.js
self.onmessage = (event) => {
  const { task, data } = event.data;
  
  if (task === 'calculate') {
    // Тяжелые вычисления (не блокируют main thread)
    const result = heavyCalculation(data);
    self.postMessage(result);
  }
};

7. Fetch API

Современный способ работы с HTTP запросами:

// Базовый запрос
async function loadData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

// С параметрами
async function createUser(userData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return await response.json();
}

// С обработкой ошибок
async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (response.ok) return response;
    } catch (error) {
      console.log(`Попытка ${i + 1} не удалась`);
    }
  }
  throw new Error('Все попытки исчерпаны');
}

8. Микротаски vs Макротаски

Есть два типа задач в Event Loop:

// Макротаска (Macrotask) - выполняется в очереди
setTimeout(() => console.log('1. Макротаска'), 0);

// Микротаска (Microtask) - выполняется перед макротаской
Promise.resolve().then(() => console.log('2. Микротаска'));

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

// Порядок вывода:
// 3. Синхронный код
// 2. Микротаска
// 1. Макротаска

Очередность в Event Loop:

1. Выполнить все синхронный код (Call Stack)
2. Выполнить все Microtasks (Promises, MutationObserver)
3. Выполнить одну Macrotask (setTimeout, fetch)
4. Выполнить все Microtasks (после Macrotask)
5. Повторить с шага 3

Практический пример: Полная асинхронная операция

async function handleUserSearch(searchTerm) {
  // Микротаска
  const validationResult = await validateInput(searchTerm);
  if (!validationResult) return;
  
  // Макротаска - сетевой запрос
  const users = await fetch(`/api/search?q=${searchTerm}`)
    .then(r => r.json());
  
  // Обновляем DOM
  users.forEach(user => {
    const item = createUserElement(user);
    // Слушатель события - асинхронное ожидание клика
    item.addEventListener('click', async () => {
      const details = await fetchUserDetails(user.id);
      displayUserModal(details);
    });
  });
}

// Макротаска - отложенное выполнение
document.getElementById('search').addEventListener('change', (e) => {
  // Дебаунс с setTimeout
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    handleUserSearch(e.target.value);
  }, 300);
});

Выводы

Асинхронность в браузере организована через Event Loop архитектуру. Есть 8 основных способов: callbacks (старо, сложно), Promises (лучше), async/await (лучше всего), event listeners (для события), setTimeout (для отложений), Web Workers (для многопоточности), Fetch API (для HTTP), микротаски и макротаски (порядок выполнения). Современные приложения используют async/await для чистого, читаемого кода и Promises.all/race для параллельных операций. Понимание Event Loop критично для оптимизации производительности.