Voici la traduction de l'article du russe vers le français, en respectant toutes les règles spécifiées :
Асинхронный JavaScript — это одна из самых важных и уникальных особенностей языка. Почему загрузка данных не "замораживает" страницу? Как работают анимации и таймеры? Почему нужны Promise и async/await? В этом уроке мы разберём всё это — от принципа работы до практического использования fetch API.
Почему JavaScript однопоточный?
JavaScript — однопоточный язык. Это значит, что он может делать только одно дело одновременно. Аналогия: один кассир в магазине. Пока он обслуживает одного покупателя — остальные ждут в очереди.
Если JavaScript мог бы делать только одно за раз — сайты бы "замерзали" при любом запросе к серверу. Нажал кнопку "загрузить" — страница висит и не реагирует, пока не загрузятся данные. Ужасно!
Решение — асинхронность. Аналогия с рестораном быстрого питания: кассир принимает ваш заказ и отдаёт вам номерок. Пока повар готовит еду — кассир обслуживает следующих. Когда еда готова — вас вызывают по номерку. Кассир (JavaScript) не стоит и не ждёт у плиты — он продолжает работать!
Событийный цикл (Event Loop)
Чтобы понять асинхронность, нужно понять, как JavaScript управляет задачами:
console.log("1. Начало");
setTimeout(function() {
console.log("3. setTimeout (через 0мс)");
}, 0); // Даже с 0 мс — выполнится после синхронного кода!
console.log("2. Конец синхронного кода");
// Вывод:
// 1. Начало
// 2. Конец синхронного кода
// 3. setTimeout (через 0мс)
Вот что происходит: JavaScript сначала выполняет весь синхронный код сверху вниз. setTimeout помещается в очередь задач ("отложить на потом"). После завершения синхронного кода — берётся первое задание из очереди. Это и есть Event Loop — цикл событий.
console.log("Старт");
// Имитация запроса к серверу (занимает 2 секунды):
setTimeout(() => console.log("Данные с сервера получены!"), 2000);
// Пока ждём ответ — делаем другие дела:
console.log("Делаем что-то ещё, пока ждём...");
console.log("И ещё что-то...");
console.log("Сайт работает, не замёрз!");
// Через 2 секунды: "Данные с сервера получены!"
Callback-функции и Callback Hell
Первый способ работы с асинхронным кодом — колбэки. Мы уже знаем их из прошлых уроков. Идея: когда асинхронная операция завершится, вызови эту функцию (позвони обратно).
// Простой колбэк — загрузить данные пользователя:
function загрузитьПользователя(id, колбэк) {
setTimeout(function() {
// Имитация получения данных с сервера:
let пользователь = { id: id, имя: "Аня", возраст: 20 };
колбэк(пользователь);
}, 1000);
}
загрузитьПользователя(1, function(пользователь) {
console.log(`Загружен: ${пользователь.имя}`);
});
Пока всё хорошо. Но что если нужно сделать несколько последовательных запросов? Загрузить пользователя, потом его заказы, потом детали заказа... Получается Callback Hell (ад колбэков):
// Callback Hell — пирамида из ада:
загрузитьПользователя(1, function(пользователь) {
console.log("Пользователь:", пользователь.имя);
загрузитьЗаказы(пользователь.id, function(заказы) {
console.log("Заказов:", заказы.length);
загрузитьТовары(заказы[0].id, function(товары) {
console.log("Товаров в заказе:", товары.length);
проверитьНаличие(товары[0].id, function(наличие) {
console.log("В наличии:", наличие);
// И ещё глубже... и глубже...
// Код уходит вправо, читать невозможно, поддерживать невозможно
});
});
});
});
Это называют "пирамидой смерти" — каждый уровень вложенности сдвигает код вправо, код становится нечитаемым, и обработка ошибок превращается в кошмар. Именно эту проблему решили Promise.
Promise: "Я обещаю вернуть результат"
Promise (промис) — это объект, представляющий результат асинхронной операции. Аналогия: вы заказали пиццу. Курьер дал вам "квиток о заказе" (это промис). Пиццы ещё нет, но у вас есть обещание. Промис может быть в трёх состояниях:
- pending (ожидание) — пицца ещё готовится
- fulfilled (выполнен) — пиццу привезли, всё отлично
- rejected (отклонён) — ресторан закрылся, заказ отменён
// Создание Promise:
let мойПромис = new Promise(function(resolve, reject) {
// Асинхронная операция (имитируем запрос):
setTimeout(function() {
let успех = true; // Представим, что запрос успешен
if (успех) {
resolve("Данные получены!"); // Успех — вызвать resolve с результатом
} else {
reject("Ошибка соединения"); // Ошибка — вызвать reject с причиной
}
}, 2000);
});
// Использование Promise:
мойПромис
.then(function(результат) {
console.log("Успех:", результат); // "Данные получены!"
})
.catch(function(ошибка) {
console.log("Ошибка:", ошибка); // Если reject
})
.finally(function() {
console.log("Выполнилось в любом случае"); // И при успехе, и при ошибке
});
Разбор:
new Promise((resolve, reject) => {...})— создаём промис. Внутри выполняется асинхронная работа.resolve(данные)— сигнал "всё хорошо, вот результат"reject(ошибка)— сигнал "что-то пошло не так".then(колбэк)— что делать при успехе.catch(колбэк)— что делать при ошибке.finally(колбэк)— выполнится всегда
Цепочки промисов
Главное преимущество промисов — они решают проблему callback hell. Вместо вложенных колбэков — читаемая цепочка:
function загрузитьПользователя(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id: id, имя: "Аня" }), 500);
});
}
function загрузитьЗаказы(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([{ id: 1, сумма: 500 }, { id: 2, сумма: 1200 }]), 500);
});
}
function загрузитьДетали(orderId) {
return new Promise((resolve) => {
setTimeout(() => resolve({ товар: "Ноутбук", количество: 1 }), 500);
});
}
// Цепочка промисов — читабельно!
загрузитьПользователя(1)
.then(пользователь => {
console.log("Пользователь:", пользователь.имя);
return загрузитьЗаказы(пользователь.id);
})
.then(заказы => {
console.log("Заказов:", заказы.length);
return загрузитьДетали(заказы[0].id);
})
.then(детали => {
console.log("Детали:", детали.товар);
})
.catch(ошибка => {
console.error("Что-то пошло не так:", ошибка);
});
Promise.all и Promise.race
// Promise.all — ждёт ВСЕ промисы параллельно:
let запрос1 = new Promise(r => setTimeout(() => r("Данные 1"), 1000));
let запрос2 = new Promise(r => setTimeout(() => r("Данные 2"), 2000));
let запрос3 = new Promise(r => setTimeout(() => r("Данные 3"), 500));
Promise.all([запрос1, запрос2, запрос3])
.then(результаты => {
console.log(результаты); // ["Данные 1", "Данные 2", "Данные 3"]
// Ждёт самый медленный (2 секунды), потом возвращает все вместе
});
// Если хоть один reject — всё Promise.all идёт в catch!
// Promise.race — возвращает результат первого завершившегося:
Promise.race([запрос1, запрос2, запрос3])
.then(результат => {
console.log(результат); // "Данные 3" (он завершился первым за 500мс)
});
// Promise.allSettled — ждёт все, не прерывается на ошибках:
Promise.allSettled([запрос1, запрос2, запрос3])
.then(результаты => {
результаты.forEach(р => {
if (р.status === "fulfilled") console.log("Успех:", р.value);
else console.log("Ошибка:", р.reason);
});
});
async/await: сахар поверх промисов
async/await — это синтаксический сахар над промисами, делающий асинхронный код похожим на синхронный. Проще говоря: вместо .then().then() — обычный код сверху вниз, только с await перед асинхронными операциями.
// Функция с async — всегда возвращает промис:
async function получитьДанные() {
// await — "подождать" пока промис разрешится, потом продолжить:
let пользователь = await загрузитьПользователя(1);
console.log("Пользователь:", пользователь.имя);
let заказы = await загрузитьЗаказы(пользователь.id);
console.log("Заказов:", заказы.length);
let детали = await загрузитьДетали(заказы[0].id);
console.log("Детали:", детали.товар);
return детали; // async функция возвращает промис с этим значением
}
// Вызов:
получитьДанные();
// или:
получитьДанные().then(детали => console.log("Готово!", детали));
Сравнение: то же самое через .then() занимало много строк с вложенностями. С async/await — читается как синхронный код сверху вниз!
Обработка ошибок в async/await
async function безопасныйЗапрос(url) {
try {
// Пробуем выполнить асинхронную операцию:
let ответ = await fetch(url);
if (!ответ.ok) {
throw new Error(`HTTP ошибка: ${ответ.status}`);
}
let данные = await ответ.json();
return данные;
} catch (ошибка) {
// Ловим любые ошибки — и сетевые, и наши throw:
console.error("Ошибка запроса:", ошибка.message);
return null; // Возвращаем null при ошибке
} finally {
console.log("Запрос завершён (с любым исходом)");
}
}
async function main() {
let данные = await безопасныйЗапрос("https://api.example.com/users");
if (данные) {
console.log("Получено пользователей:", данные.length);
} else {
console.log("Не удалось загрузить данные");
}
}
main();
fetch API: запросы к серверу
fetch — это встроенная функция JavaScript для выполнения HTTP-запросов (получения данных с серверов). Возвращает промис.
// Базовый GET запрос (получить данные):
async function загрузитьПользователей() {
try {
// JSONPlaceholder — бесплатный тестовый API:
let ответ = await fetch("https://jsonplaceholder.typicode.com/users");
// Проверяем статус ответа:
if (!ответ.ok) {
throw new Error(`Ошибка: ${ответ.status}`);
}
// Парсим JSON:
let пользователи = await ответ.json();
console.log(`Загружено ${пользователи.length} пользователей:`);
пользователи.slice(0, 3).forEach(п => {
console.log(` - ${п.name} (${п.email})`);
});
} catch (ошибка) {
console.error("Ошибка загрузки:", ошибка.message);
}
}
загрузитьПользователей();
// POST запрос (отправить данные):
async function создатьПост(заголовок, текст) {
try {
let ответ = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: заголовок,
body: текст,
userId: 1
})
});
let новыйПост = await ответ.json();
console.log("Пост создан! ID:", новыйПост.id);
return новыйПост;
} catch (ошибка) {
console.error("Ошибка создания поста:", ошибка.message);
}
}
создатьПост("Мой первый пост", "Это текст поста");
Разбор fetch: первый аргумент — URL. Второй (необязательный) — настройки: метод (GET/POST/PUT/DELETE), заголовки, тело запроса. ответ.json() — тоже промис, нужно ждать. ответ.ok — true если статус 200-299.
Реальный пример: погодное приложение
// Приложение для получения погоды через Open-Meteo API (бесплатный, без ключа):
async function получитьПогоду(город, широта, долгота) {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${широта}&longitude=${долгота}¤t_weather=true`;
try {
console.log(`Загружаю погоду для ${город}...`);
const ответ = await fetch(url);
if (!ответ.ok) throw new Error("Не удалось получить погоду");
const данные = await ответ.json();
const погода = данные.current_weather;
const описаниеКода = {
0: "Ясно", 1: "Почти ясно", 2: "Переменная облачность",
3: "Пасмурно", 61: "Дождь", 71: "Снег", 95: "Гроза"
};
const описание = описаниеКода[погода.weathercode] || "Неизвестно";
console.log(`
🌤 Погода в ${город}:`);
console.log(` Температура: ${погода.temperature}°C`);
console.log(` Ветер: ${погода.windspeed} км/ч`);
console.log(` Описание: ${описание}`);
return погода;
} catch (ошибка) {
console.error(`❌ Ошибка для ${город}:`, ошибка.message);
return null;
}
}
// Параллельная загрузка погоды в нескольких городах:
async function погодаНескольких() {
const города = [
{ имя: "Москва", lat: 55.75, lon: 37.62 },
{ имя: "Санкт-Петербург", lat: 59.93, lon: 30.32 },
{ имя: "Новосибирск", lat: 55.02, lon: 82.92 }
];
// Запускаем все запросы параллельно:
const результаты = await Promise.all(
города.map(г => получитьПогоду(г.имя, г.lat, г.lon))
);
console.log("
✅ Все данные загружены!");
}
погодаНескольких();
async/await vs Promise.then — когда что использовать?
| Ситуация | Рекомендация |
|---|---|
| Последовательные операции | async/await — читабельнее |
| Параллельные операции | Promise.all — эффективнее |
| Обработка ошибок | try/catch с async/await — удобнее |
| Простой одиночный промис | .then().catch() — лаконично |
| Работа с событиями | Колбэки — традиционный подход |
Типичные ошибки новичков
- Забыть await.
let данные = fetch(url)— вы получите промис, а не данные! Правильно:let данные = await fetch(url). - await вне async функции. await работает только внутри async функций. На верхнем уровне (в современных браузерах/Node.js) можно, но внутри обычной функции — ошибка.
- Не обрабатывать ошибки. Всегда оборачивайте await в try/catch. Сетевые ошибки случаются — сервер недоступен, интернет пропал.
- Последовательный await вместо параллельного. Если две операции независимы — запускайте их параллельно через Promise.all, не последовательно.
- Не проверять ответ fetch. fetch не выбрасывает ошибку при статусах 404, 500 и т.д. Всегда проверяйте
response.ok.
Итого
В этом уроке мы узнали:
- JavaScript однопоточный, но использует Event Loop для асинхронности — код не замерзает при ожидании.
- Callback — первый подход к асинхронности, приводит к callback hell при вложенности.
- Promise — объект, представляющий будущий результат. Три состояния: pending, fulfilled, rejected.
- .then()/.catch()/.finally() — обработка результатов промиса в цепочке.
- Promise.all — запустить все промисы параллельно, ждать завершения всех.
- async/await — синтаксический сахар над промисами. Делает асинхронный код похожим на синхронный.
- try/catch — обработка ошибок в async функциях.
- fetch API — встроенный инструмент для HTTP-запросов, возвращает промис.
Поздравляем! Вы прошли все шесть уроков по основам JavaScript. Теперь у вас есть фундамент для изучения фреймворков (React, Vue), серверной части (Node.js, Express) и всего остального увлекательного мира JavaScript-разработки!