Модуль 8: Асинхронность и API
Мастерство работы с асинхронным кодом, Promises, async/await и Fetch API.
Синхронность vs Асинхронность
Синхронный код
// Синхронный — выполняется по порядку
console.log('1');
console.log('2');
console.log('3');
// Вывод: 1, 2, 3
// Блокирующая операция
const start = Date.now();
while (Date.now() - start < 3000) {} // Блокирует на 3 сек!
console.log('Готово'); // Только через 3 секунды
Асинхронный код
// Асинхронный — не блокирует
console.log('1');
setTimeout(() => {
console.log('2'); // Выполнится позже
}, 1000);
console.log('3');
// Вывод: 1, 3, 2
// Event Loop обрабатывает асинхронные операции
Event Loop
Как работает Event Loop
- Call Stack — выполняет синхронный код
- Web APIs — обрабатывают async операции (setTimeout, fetch)
- Task Queue (Macrotask) — setTimeout, setInterval, I/O
- Microtask Queue — Promise, queueMicrotask (приоритет!)
- Event Loop переносит задачи из очередей в Call Stack
console.log('1'); // Синхронно
setTimeout(() => console.log('2'), 0); // Macrotask
Promise.resolve().then(() => console.log('3')); // Microtask
console.log('4'); // Синхронно
// Вывод: 1, 4, 3, 2
// Microtask выполняется раньше macrotask!
Callbacks
Что такое callback
// Callback — функция, переданная как аргумент
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'Иван' };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data.name); // "Иван" через 1 сек
});
Callback Hell
// Проблема: вложенные callbacks
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
getProduct(details.productId, (product) => {
console.log(product);
// Это "Callback Hell" или "Pyramid of Doom"
});
});
});
});
Проблемы callbacks
- Вложенность (Callback Hell)
- Сложная обработка ошибок
- Трудно читать и поддерживать
Promises
Что такое Promise
Promise — объект, представляющий результат асинхронной операции.
// Создание Promise
const promise = new Promise((resolve, reject) => {
// Асинхронная операция
setTimeout(() => {
const success = true;
if (success) {
resolve({ data: 'Данные' }); // Успех
} else {
reject(new Error('Ошибка')); // Неудача
}
}, 1000);
});
// Использование
promise
.then(result => console.log(result))
.catch(error => console.error(error));
Состояния Promise
| Состояние | Описание |
|---|---|
pending |
Ожидание (начальное) |
fulfilled |
Выполнено успешно (resolve) |
rejected |
Отклонено (reject) |
Цепочки Promise (Chaining)
// Решение Callback Hell
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getProduct(details.productId))
.then(product => console.log(product))
.catch(error => console.error('Ошибка:', error));
// Каждый .then возвращает новый Promise
fetch('/api/user')
.then(response => response.json()) // Возвращает Promise
.then(data => {
console.log(data);
return data.id; // Обычное значение — оборачивается в Promise
})
.then(id => fetch(`/api/orders/${id}`))
.then(response => response.json())
.then(orders => console.log(orders));
Статические методы Promise
// Promise.resolve / Promise.reject
Promise.resolve(42).then(x => console.log(x)); // 42
Promise.reject(new Error('Oops')).catch(e => console.error(e));
// Promise.all — ждёт ВСЕ, падает при первой ошибке
const results = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// Promise.allSettled — ждёт ВСЕ, не падает
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/might-fail')
]);
// [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]
// Promise.race — первый завершившийся (успех или ошибка)
const fastest = await Promise.race([
fetch('/api/server1'),
fetch('/api/server2')
]);
// Promise.any — первый УСПЕШНЫЙ
const firstSuccess = await Promise.any([
fetch('/api/might-fail'),
fetch('/api/might-succeed')
]);
async/await
Синтаксис
// async функция всегда возвращает Promise
async function getData() {
return 'данные';
}
getData().then(console.log); // 'данные'
// await приостанавливает выполнение до resolve
async function fetchUser() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
// Сравнение
// Promise:
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(user => user);
}
// async/await:
async function getUser() {
const res = await fetch('/api/user');
const user = await res.json();
return user;
}
Последовательные vs Параллельные запросы
// Последовательно (медленно!)
async function fetchAll() {
const users = await fetch('/api/users'); // 1 сек
const posts = await fetch('/api/posts'); // + 1 сек
const comments = await fetch('/api/comments'); // + 1 сек
// Итого: 3 секунды
}
// Параллельно (быстро!)
async function fetchAll() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
// Итого: ~1 секунда (самый медленный)
}
// Или запустить, потом собрать
async function fetchAll() {
// Запускаем ВСЕ сразу
const usersPromise = fetch('/api/users');
const postsPromise = fetch('/api/posts');
const commentsPromise = fetch('/api/comments');
// Ждём результаты
const users = await usersPromise;
const posts = await postsPromise;
const comments = await commentsPromise;
}
await в циклах
const ids = [1, 2, 3, 4, 5];
// Последовательно
for (const id of ids) {
const user = await fetchUser(id); // Ждёт каждого
console.log(user);
}
// Параллельно
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
console.log(users);
// Параллельно с обработкой каждого
await Promise.all(ids.map(async (id) => {
const user = await fetchUser(id);
await processUser(user);
}));
Обработка ошибок
try/catch с async/await
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'TypeError') {
console.error('Сетевая ошибка:', error.message);
} else {
console.error('Ошибка:', error.message);
}
throw error; // Пробрасываем дальше
} finally {
console.log('Запрос завершён');
}
}
Обработка ошибок в Promise.all
// Promise.all падает при ПЕРВОЙ ошибке
try {
const results = await Promise.all([
fetch('/api/1'),
fetch('/api/might-fail'),
fetch('/api/3')
]);
} catch (error) {
// Сработает если ЛЮБОЙ запрос упадёт
}
// Promise.allSettled не падает
const results = await Promise.allSettled([
fetch('/api/1'),
fetch('/api/might-fail'),
fetch('/api/3')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Запрос ${index}: успех`, result.value);
} else {
console.error(`Запрос ${index}: ошибка`, result.reason);
}
});
Глобальная обработка
// Необработанные Promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Необработанная ошибка Promise:', event.reason);
// Отправить в систему логирования
sendToErrorTracking(event.reason);
});
// Глобальный обработчик ошибок
window.addEventListener('error', (event) => {
console.error('Глобальная ошибка:', event.error);
});
Fetch API
Базовое использование
// GET запрос
const response = await fetch('https://api.example.com/users');
const users = await response.json();
// POST запрос
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Иван',
email: 'ivan@example.com'
})
});
// Проверка статуса
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const newUser = await response.json();
Объект Response
const response = await fetch('/api/data');
// Свойства
response.ok; // true если статус 200-299
response.status; // 200, 404, 500...
response.statusText; // "OK", "Not Found"...
response.headers; // Headers объект
response.url; // URL запроса
response.type; // "basic", "cors", "opaque"
// Методы чтения body (можно вызвать только ОДИН раз!)
const json = await response.json(); // JSON → Object
const text = await response.text(); // String
const blob = await response.blob(); // Blob (файлы)
const buffer = await response.arrayBuffer(); // Binary
const formData = await response.formData(); // FormData
// Клонирование для многократного чтения
const clone = response.clone();
const json1 = await response.json();
const json2 = await clone.json();
Headers
// Чтение заголовков ответа
response.headers.get('Content-Type');
response.headers.has('Authorization');
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
// Создание заголовков для запроса
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');
// Или объектом
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
};
Опции fetch
const response = await fetch(url, {
method: 'POST', // GET, POST, PUT, DELETE, PATCH
headers: { ... },
body: JSON.stringify(data),
mode: 'cors', // cors, no-cors, same-origin
credentials: 'include', // include, same-origin, omit
cache: 'no-cache', // default, no-cache, reload, force-cache
redirect: 'follow', // follow, error, manual
referrerPolicy: 'no-referrer',
signal: abortController.signal // Для отмены
});
Работа с REST API
CRUD операции
const API_URL = 'https://api.example.com';
// CREATE (POST)
async function createUser(userData) {
const response = await fetch(`${API_URL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
// READ (GET)
async function getUser(id) {
const response = await fetch(`${API_URL}/users/${id}`);
return response.json();
}
async function getUsers(params = {}) {
const query = new URLSearchParams(params).toString();
const response = await fetch(`${API_URL}/users?${query}`);
return response.json();
}
// UPDATE (PUT/PATCH)
async function updateUser(id, userData) {
const response = await fetch(`${API_URL}/users/${id}`, {
method: 'PATCH', // или PUT для полной замены
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}
// DELETE
async function deleteUser(id) {
const response = await fetch(`${API_URL}/users/${id}`, {
method: 'DELETE'
});
return response.ok;
}
API клиент
class ApiClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
...options.headers
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.message || 'API Error');
}
return response.json();
}
get(endpoint, params) {
const query = params ? `?${new URLSearchParams(params)}` : '';
return this.request(`${endpoint}${query}`);
}
post(endpoint, data) {
return this.request(endpoint, { method: 'POST', body: data });
}
put(endpoint, data) {
return this.request(endpoint, { method: 'PUT', body: data });
}
patch(endpoint, data) {
return this.request(endpoint, { method: 'PATCH', body: data });
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
}
// Использование
const api = new ApiClient('https://api.example.com');
const users = await api.get('/users', { page: 1, limit: 10 });
const newUser = await api.post('/users', { name: 'Иван' });
Продвинутый Fetch
Отмена запроса (AbortController)
const controller = new AbortController();
// Запрос с возможностью отмены
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Запрос отменён');
}
});
// Отмена через 5 секунд (timeout)
setTimeout(() => controller.abort(), 5000);
// Или по действию пользователя
button.addEventListener('click', () => controller.abort());
// Timeout helper
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(id);
}
}
Загрузка файлов
// Загрузка файла
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', file.name);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData // Не устанавливайте Content-Type!
});
return response.json();
}
// С прогрессом (через XMLHttpRequest)
function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
onProgress(percent);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
Скачивание файлов
async function downloadFile(url, filename) {
const response = await fetch(url);
const blob = await response.blob();
// Создаём ссылку для скачивания
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
// Освобождаем память
URL.revokeObjectURL(link.href);
}
Retry логика
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// Retry только для определённых статусов
if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
return response; // 4xx — не retry
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Попытка ${i + 1} не удалась, повтор через ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
delay *= 2; // Exponential backoff
}
}
}
Параллельные запросы
Promise.all
// Загрузка нескольких ресурсов одновременно
async function loadDashboard() {
const [user, orders, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
]);
return { user, orders, notifications };
}
// С обработкой ошибок для каждого
async function loadDashboardSafe() {
const results = await Promise.allSettled([
fetch('/api/user').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/notifications').then(r => r.json())
]);
return {
user: results[0].status === 'fulfilled' ? results[0].value : null,
orders: results[1].status === 'fulfilled' ? results[1].value : [],
notifications: results[2].status === 'fulfilled' ? results[2].value : []
};
}
Ограничение параллельности
// Выполнить массив промисов с ограничением
async function parallelLimit(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
if (limit <= tasks.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// Использование
const urls = [...]; // 100 URL
const results = await parallelLimit(
urls.map(url => () => fetch(url).then(r => r.json())),
5 // Максимум 5 одновременных запросов
);
Кэширование
Простой кэш в памяти
const cache = new Map();
async function fetchWithCache(url, ttl = 60000) {
const cached = cache.get(url);
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, { data, timestamp: Date.now() });
return data;
}
// Очистка старых записей
function clearExpiredCache(ttl) {
const now = Date.now();
for (const [key, value] of cache) {
if (now - value.timestamp > ttl) {
cache.delete(key);
}
}
}
Cache API
// Открыть кэш
const cache = await caches.open('api-cache-v1');
// Добавить в кэш
await cache.put('/api/data', response);
await cache.add('/api/data'); // fetch + put
// Получить из кэша
const cachedResponse = await cache.match('/api/data');
// Стратегия: Cache First, Network Fallback
async function cacheFirst(url) {
const cachedResponse = await caches.match(url);
if (cachedResponse) return cachedResponse;
const response = await fetch(url);
const cache = await caches.open('api-cache');
cache.put(url, response.clone());
return response;
}
// Стратегия: Network First, Cache Fallback
async function networkFirst(url) {
try {
const response = await fetch(url);
const cache = await caches.open('api-cache');
cache.put(url, response.clone());
return response;
} catch {
return caches.match(url);
}
}
WebSockets
Базовое использование
const ws = new WebSocket('wss://api.example.com/ws');
// События
ws.addEventListener('open', () => {
console.log('Соединение открыто');
ws.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
});
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Получено:', data);
});
ws.addEventListener('close', (event) => {
console.log(`Соединение закрыто: ${event.code} ${event.reason}`);
});
ws.addEventListener('error', (error) => {
console.error('Ошибка WebSocket:', error);
});
// Отправка сообщений
ws.send(JSON.stringify({ type: 'message', text: 'Привет!' }));
// Закрытие
ws.close(1000, 'Нормальное закрытие');
WebSocket с реконнектом
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 1000;
this.maxReconnectInterval = options.maxReconnectInterval || 30000;
this.handlers = { message: [], open: [], close: [], error: [] };
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectInterval = 1000;
this.handlers.open.forEach(h => h());
};
this.ws.onmessage = (e) => {
this.handlers.message.forEach(h => h(JSON.parse(e.data)));
};
this.ws.onclose = () => {
this.handlers.close.forEach(h => h());
this.reconnect();
};
this.ws.onerror = (e) => {
this.handlers.error.forEach(h => h(e));
};
}
reconnect() {
setTimeout(() => {
console.log('Переподключение...');
this.connect();
this.reconnectInterval = Math.min(
this.reconnectInterval * 2,
this.maxReconnectInterval
);
}, this.reconnectInterval);
}
on(event, handler) {
this.handlers[event].push(handler);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
// Использование
const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('message', (data) => console.log(data));
Реальные проекты
Поиск с debounce
const searchInput = document.getElementById('search');
const resultsContainer = document.getElementById('results');
let controller = null;
const search = debounce(async (query) => {
// Отменяем предыдущий запрос
if (controller) controller.abort();
controller = new AbortController();
if (!query.trim()) {
resultsContainer.innerHTML = '';
return;
}
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: controller.signal }
);
const results = await response.json();
renderResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Ошибка поиска:', error);
}
}
}, 300);
searchInput.addEventListener('input', (e) => search(e.target.value));
Бесконечный скролл
let page = 1;
let loading = false;
let hasMore = true;
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading && hasMore) {
loading = true;
showLoader();
try {
const items = await fetchItems(page);
if (items.length === 0) {
hasMore = false;
} else {
appendItems(items);
page++;
}
} catch (error) {
console.error('Ошибка загрузки:', error);
} finally {
loading = false;
hideLoader();
}
}
});
// Наблюдаем за элементом в конце списка
observer.observe(document.querySelector('.load-trigger'));
Итоговый проект
Создайте приложение новостей с:
- Загрузкой новостей из публичного API
- Поиском с debounce
- Бесконечным скроллом
- Кэшированием
- Обработкой ошибок и retry
- Индикаторами загрузки