Введение: Почему код иногда ждёт?
Представь, что ты заказываешь пиццу по телефону. Ты звонишь, делаешь заказ, а потом... что ты делаешь? Ждёшь, пока пиццу приготовят и привезут, ничего не делая? Конечно нет! Ты можешь смотреть телевизор, делать уроки или играть в компьютерную игру. Когда пицца приезжает, ты идёшь открывать дверь.
Точно так же работает и JavaScript в браузере. Когда программа выполняет что-то, что занимает время (например, загрузка картинки из интернета или запрос к серверу), она не замирает и не ждёт. Она продолжает работать дальше, а когда "пицца приезжает" — выполняет специальную функцию-обработчик.
Это и есть асинхронность (от греческого "асин" — не вместе, и "хронос" — время). В этом уроке мы научимся писать такой код, который не заставляет пользователя ждать.
Синхронный код vs Асинхронный код
Давай сначала посмотрим, как работает обычный синхронный код. Он выполняется строка за строкой, как очередь в магазине.
// Синхронный код — всё по порядку
console.log("Шаг 1: Начинаем");
console.log("Шаг 2: Делаем что-то");
console.log("Шаг 3: Заканчиваем");
// Вывод будет строго по порядку:
// Шаг 1: Начинаем
// Шаг 2: Делаем что-то
// Шаг 3: Заканчиваем
А теперь представь, что нам нужно загрузить данные из интернета. Если бы это было синхронно, программа бы замерла на время загрузки:
// ПЛОХОЙ пример (так не работает в реальности)
console.log("Начинаем загрузку...");
let данные = загрузитьИзИнтернета(); // Программа зависла бы здесь на 2 секунды!
console.log("Загрузка завершена!");
console.log("Данные:", данные);
К счастью, JavaScript умеет делать такие вещи асинхронно. Вот как это выглядит на самом деле:
// Асинхронный код — программа не ждёт
console.log("Начинаем загрузку...");
setTimeout(() => {
console.log("Прошло 2 секунды!");
}, 2000); // 2000 миллисекунд = 2 секунды
console.log("А это выполнится сразу, не дожидаясь таймера!");
// Вывод:
// Начинаем загрузку...
// А это выполнится сразу, не дожидаясь таймера!
// (через 2 секунды) Прошло 2 секунды!
Обрати внимание: console.log("А это выполнится сразу...") сработал раньше, чем таймер! Программа не остановилась, а пошла дальше.
Аналогия из жизни: Кофе и бутерброд
Представь, что ты готовишь завтрак. Ты ставишь чайник (это занимает 3 минуты). Вместо того чтобы стоять и смотреть на чайник, ты делаешь бутерброд. Когда чайник закипает — ты выключаешь его.
- Синхронный подход: Поставить чайник → ждать 3 минуты → сделать бутерброд → есть. (Потеряно 3 минуты!)
- Асинхронный подход: Поставить чайник → делать бутерброд → чайник закипел (выключить) → есть. (Время используется эффективно!)
В программировании то же самое: асинхронный код позволяет делать полезные вещи, пока ждёшь ответа от сервера или загрузки файла.
Основные способы работы с асинхронностью
В JavaScript есть три основных способа работать с асинхронным кодом. Мы рассмотрим их по порядку: от самого старого к самому современному.
1. Callback-функции (старый способ)
Callback (произносится "колбэк") — это функция, которая вызывается, когда асинхронная операция завершится. Буквально "перезвонить".
// Пример с setTimeout — это простейший callback
console.log("Запускаем таймер...");
setTimeout(function() {
console.log("Таймер сработал! Прошло 3 секунды.");
}, 3000);
console.log("Таймер запущен, продолжаем делать другие дела.");
// Вывод:
// Запускаем таймер...
// Таймер запущен, продолжаем делать другие дела.
// (через 3 секунды) Таймер сработал! Прошло 3 секунды.
Но у колбэков есть проблема — если нужно сделать несколько асинхронных операций подряд, код превращается в "ад колбэков":
// "Ад колбэков" — когда один callback внутри другого
console.log("Начинаем цепочку...");
setTimeout(() => {
console.log("Шаг 1 выполнен через 1 сек");
setTimeout(() => {
console.log("Шаг 2 выполнен через 2 сек");
setTimeout(() => {
console.log("Шаг 3 выполнен через 3 сек");
// Если бы нужен был ещё один шаг — было бы ещё глубже!
}, 1000);
}, 1000);
}, 1000);
// Такой код трудно читать и поддерживать
Это как если бы тебе нужно было сделать заказ, потом подтвердить его, потом оплатить, потом получить чек — и каждый шаг требовал бы нового звонка. Ужас!
2. Promise (Обещания) — современный способ
Promise (произносится "промис") — это объект, который представляет результат асинхронной операции. Буквально "обещание".
Представь, что ты заказываешь книгу в интернет-магазине. Ты получаешь "обещание", что книга придёт. У этого обещания есть три состояния:
- Pending (в ожидании) — книга ещё не пришла, но обещание есть
- Resolved (выполнено) — книга пришла, всё хорошо
- Rejected (отклонено) — книга не пришла (например, закончилась на складе)
Давай создадим свой первый Promise:
// Создаём новое обещание
const пиццаОбещание = new Promise((resolve, reject) => {
// Здесь происходит асинхронная операция
let пиццаГотова = true; // Представим, что пиццу приготовили
if (пиццаГотова) {
resolve("Пицца приехала! 🍕"); // Всё хорошо
} else {
reject("Пиццу не приготовили 😢"); // Что-то пошло не так
}
});
// Используем обещание
console.log("Заказали пиццу...");
пиццаОбещание
.then((сообщение) => {
console.log(сообщение); // Выполнится, если resolve
})
.catch((ошибка) => {
console.log(ошибка); // Выполнится, если reject
});
console.log("Ждём пиццу, делаем другие дела...");
// Вывод:
// Заказали пиццу...
// Ждём пиццу, делаем другие дела...
// Пицца приехала! 🍕
А теперь более реальный пример — имитация загрузки данных с сервера:
// Функция, которая имитирует загрузку данных
function загрузитьДанные(url) {
return new Promise((resolve, reject) => {
console.log(`Начинаем загрузку с ${url}...`);
// Имитируем задержку сети (1-3 секунды)
let времяЗагрузки = Math.random() * 2000 + 1000;
setTimeout(() => {
// В 90% случаев загрузка успешна
if (Math.random() < 0.9) {
let данные = {
пользователь: "Алиса",
возраст: 14,
город: "Москва"
};
resolve(данные); // Всё хорошо, возвращаем данные
} else {
reject("Ошибка загрузки! Сервер недоступен."); // Ошибка
}
}, времяЗагрузки);
});
}
// Используем нашу функцию
console.log("Программа начата");
загрузитьДанные("https://api.example.com/user")
.then((данные) => {
console.log("Данные получены:", данные);
console.log(`Привет, ${данные.пользователь}!`);
})
.catch((ошибка) => {
console.error("Упс! Ошибка:", ошибка);
})
.finally(() => {
console.log("Операция завершена (успешно или с ошибкой)");
});
console.log("Программа продолжается, не ждёт загрузку...");
Обрати внимание на finally — он выполняется в любом случае, как и в реальной жизни (например, закрыть дверь после того, как пришла пицца, даже если её не привезли).
Цепочки Promise — избавляемся от "ада колбэков"
Promise можно объединять в цепочки, что делает код чистым и понятным:
// Вместо вложенных колбэков — цепочка Promise
function шаг1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Шаг 1: Получили данные от сервера");
resolve({ id: 1, имя: "Алиса" });
}, 1000);
});
}
function шаг2(пользователь) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Шаг 2: Обработали данные пользователя");
resolve({ ...пользователь, возраст: 14 });
}, 1000);
});
}
function шаг3(пользователь) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Шаг 3: Сохранили в базу данных");
resolve(`Пользователь ${пользователь.имя} сохранён!`);
}, 1000);
});
}
// Используем цепочку — красиво и понятно!
console.log("Начинаем обработку...");
шаг1()
.then(пользователь => шаг2(пользователь))
.then(пользователь => шаг3(пользователь))
.then(результат => {
console.log("Результат:", результат);
})
.catch(ошибка => {
console.error("Ошибка в цепочке:", ошибка);
});
console.log("Программа продолжает работу...");
Сравни с "адом колбэков" — разница огромная! Код читается сверху вниз, как обычный синхронный.
3. Async/Await — самый современный способ
Async/await (произносится "эсинк эвэйт") — это синтаксический сахар над Promise. Он позволяет писать асинхронный код так, будто он синхронный. Это как читать книгу: ты ждёшь, пока перевернётся страница, но не засыпаешь над книгой.
Ключевые слова:
- async — ставится перед функцией, чтобы сделать её асинхронной
- await — ждёт выполнения Promise (но не блокирует всю программу!)
// Пример с async/await
async function приготовитьЗавтрак() {
console.log("Начинаем готовить завтрак...");
// await заставляет ждать, но только внутри этой функции
let чай = await сделатьЧай();
console.log("Чай готов!", чай);
let бутерброд = await сделатьБутерброд();
console.log("Бутерброд готов!", бутерброд);
console.log("Завтрак готов! Приятного аппетита!");
return "Завтрак подан!";
}
// Функции, возвращающие Promise
function сделатьЧай() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("☕ Горячий чай с лимоном");
}, 2000);
});
}
function сделатьБутерброд() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("🥪 Бутерброд с сыром и ветчиной");
}, 1500);
});
}
// Вызываем асинхронную функцию
console.log("Утро началось!");
приготовитьЗавтрак().then(результат => {
console.log(результат);
});
console.log("Пока завтрак готовится, чищу зубы...");
// Вывод:
// Утро началось!
// Пока завтрак готовится, чищу зубы...
// (через 2 сек) Чай готов! ☕ Горячий чай с лимоном
// (через 1.5 сек) Бутерброд готов! 🥪 Бутерброд с сыром и ветчиной
// Завтрак готов! Приятного аппетита!
// Завтрак подан!
Обрати внимание: console.log("Пока завтрак готовится, чищу зубы...") выполнился сразу, не дожидаясь завтрака! Асинхронная функция не блокирует основную программу.
А теперь — обработка ошибок с try/catch (как в обычном синхронном коде):
// Обработка ошибок с try/catch
async function загрузитьПрофиль(id) {
try {
console.log(`Загружаем профиль пользователя ${id}...`);
let ответ = await fetch(`https://api.example.com/users/${id}`);
let данные = await ответ.json();
console.log("Профиль загружен:", данные);
return данные;
} catch (ошибка) {
console.error("Не удалось загрузить профиль:", ошибка);
return null; // Возвращаем null в случае ошибки
} finally {
console.log("Попытка загрузки завершена");
}
}
// Используем
загрузитьПрофиль(42).then(профиль => {
if (профиль) {
console.log(`Привет, ${профиль.имя}!`);
}
});
С async/await код выглядит почти как обычный синхронный, но при этом остаётся асинхронным. Это делает его очень понятным для чтения.
Реальные примеры использования
Давай посмотрим на реальные ситуации, где асинхронность используется каждый день.
Загрузка изображения
// Загрузка изображения с помощью Promise
function загрузитьКартинку(url) {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => {
console.log(`Изображение загружено: ${url}`);
resolve(img);
};
img.onerror = () => {
reject(`Ошибка загрузки изображения: ${url}`);
};
img.src = url; // Начинаем загрузку
});
}
// Используем
async function показатьГалерею() {
let urls = [
"https://example.com/cat.jpg",
"https://example.com/dog.jpg"
];
for (let url of urls) {
try {
let img = await загрузитьКартинку(url);
document.body.appendChild(img); // Добавляем на страницу
} catch (ошибка) {
console.error(ошибка);
}
}
console.log("Все изображения загружены!");
}
Таймер обратного отсчёта
// Функция, которая ждёт указанное количество секунд
function ждать(секунды) {
return new Promise(resolve => {
setTimeout(resolve, секунды * 1000);
});
}
// Обратный отсчёт с async/await
async function обратныйОтсчёт(отЧисла) {
for (let i = отЧисла; i > 0; i--) {
console.log(i);
await ждать(1); // Ждём 1 секунду
}
console.log("🚀 Пуск!");
}
console.log("Запускаем отсчёт...");
обратныйОтсчёт(5);
console.log("Отсчёт запущен в фоне!");
// Вывод:
// Запускаем отсчёт...
// Отсчёт запущен в фоне!
// 5
// (через 1 сек) 4
// (через 1 сек) 3
// (через 1 сек) 2
// (через 1 сек) 1
// (через 1 сек) 🚀 Пуск!
Практические задания
А теперь давай закрепим знания на практике. Попробуй выполнить эти задания самостоятельно. Не бойся ошибаться — это часть обучения!
Задание 1: Симулятор загрузки файлов
Создай функцию скачатьФайл(имяФайла, размерМб), которая возвращает Promise. Загрузка каждого мегабайта занимает 1 секунду. Выведи сообщение о завершении загрузки.
Подсказка: Используй цикл с await для имитации прогресса.
// Примерный каркас для задания 1
async function скачатьФайл(имяФайла, размерМб) {
console.log(`Начинаем загрузку ${имяФайла} (${размерМб} Мб)...`);
// Твой код здесь
// Используй цикл for и await ждать(1)
console.log(`✅ ${имяФайла} загружен!`);
}
// Проверка
скачатьФайл("photo.jpg", 3);
// Должно вывести:
// Начинаем загрузку photo.jpg (3 Мб)...
// (через 1 сек) Загружено 1 из 3 Мб
// (через 1 сек) Загружено 2 из 3 Мб
// (через 1 сек) Загружено 3 из 3 Мб
// ✅ photo.jpg загружен!
Задание 2: Параллельная загрузка
У нас есть три файла, которые нужно загрузить. Используй Promise.all, чтобы загрузить их одновременно, а не по очереди. Это как заказать три пиццы в разных ресторанах одновременно — они будут готовиться параллельно.
// Функция загрузки (уже готова)
function загрузитьФайл(имя, времяМс) {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Файл ${имя} загружен за ${времяМс}мс`);
}, времяМс);
});
}
// Твоя задача — загрузить все файлы параллельно
async function загрузитьВсе() {
console.log("Начинаем параллельную загрузку...");
// Используй Promise.all
let результаты = await Promise.all([
загрузитьФайл("video.mp4", 3000),
загрузитьФайл("music.mp3", 2000),
загрузитьФайл("doc.pdf", 1000)
]);
console.log("Все файлы загружены:");
результаты.forEach(результат => console.log(результат));
}
загрузитьВсе();
// Общее время загрузки — 3 секунды (самый долгий файл)
// А не 6 секунд, если бы загружали по очереди!
Задание 3: Игра "Угадай число" с асинхронностью
Создай игру, где компьютер загадывает число от 1 до 10, а пользователь должен угадать его за 3 попытки. Используй prompt (синхронный) для ввода, но добавь асинхронную задержку между попытками, чтобы создать эффект "думающего компьютера".
// Задание 3: Угадай число
async function играУгадайЧисло() {
let загаданное = Math.floor(Math.random() * 10) + 1;
let попытки = 3;
console.log("Я загадал число от 1 до 10. Угадай его за 3 попытки!");
for (let i = 0; i < попытки; i++) {
// Добавь await для задержки (компьютер "думает")
await ждать(1); // Используй функцию ждать из примера
let ответ = prompt(`Попытка ${i + 1}: Введи число:`);
let число = Number(ответ);
// Проверь, угадал ли пользователь
// Если да — выведи поздравление и выйди из функции
// Если нет — скажи, больше или меньше загаданное число
}
console.log(`Ты не угадал! Я загадал ${загаданное}.`);
}
// Запусти игру
играУгадайЧисло();
Дополнительное задание (для самых умных): Добавь обработку ошибок — что если пользователь ввёл не число?
Заключение
Мы прошли большой путь! Давай вспомним главное:
- Асинхронность — это способность программы не ждать, а делать другие дела, пока выполняется долгая операция
- Callback — функции обратного вызова (старый способ)
- Promise — объект-обещание, который может быть выполнен или отклонён
- Async/await — самый удобный способ писать асинхронный код, похожий на синхронный
Помни: асинхронность — это суперсила JavaScript. Без неё интернет был бы ужасно медленным, потому что каждая загрузка картинки или запроса к серверу заставляла бы страницу замирать.
Теперь ты можешь писать программы, которые работают быстро и эффективно, даже когда нужно ждать данные из интернета. Поздравляю! 🎉
"Лучшая программа — та, которая не заставляет пользователя ждать. Асинхронность — ключ к этому."