- Объяснять разницу между синхронным и асинхронным кодом и работу
setTimeout - Создавать промис и обрабатывать результат через
then,catchиfinally - Писать понятный асинхронный код с
async/awaitиtry...catch - Выполнять несколько задач параллельно с
Promise.all
В кафе официант передаёт заказ на кухню, но не стоит у её двери, пока готовится плов, — он обслуживает другие столики. А когда блюдо готово, приносит его. JavaScript работает так же: ожидая медленные операции — ответ сервера, таймер, файл, — он не замораживает страницу, а занимается другими делами. Это называется асинхронным программированием.
Синхронно и асинхронно
Код, который мы писали до сих пор, был синхронным: каждая инструкция ждёт окончания предыдущей. А setTimeout(функция, мс) «откладывает» функцию, чтобы выполнить её через указанное число миллисекунд, и программа сразу переходит к следующей строке. Обрати внимание на порядок вывода:
console.log('1. Order placed');
setTimeout(() => {
console.log('4. Plov is ready');
}, 1000);
setTimeout(() => {
console.log('3. Tea is ready');
}, 0);
console.log('2. Waiter serves another table');▸ Ожидаемый результат
1. Order placed 2. Waiter serves another table 3. Tea is ready 4. Plov is ready
JavaScript одновременно делает только одно дело. Готовые колбэки ждут в очереди, а цикл событий берёт их оттуда по одному, когда текущий синхронный код закончился. Поэтому даже setTimeout с задержкой 0 мс срабатывает после всего синхронного кода.
Колбэки и их проблема
Самый старый способ получить результат асинхронной работы — передать колбэк: «вызови эту функцию, когда будет готово». Если несколько задач должны идти одна за другой, колбэки вкладываются друг в друга, и код растёт вправо «лесенкой». Программисты называют это callback hell («ад колбэков»).
function cook(dish, callback) {
setTimeout(() => callback(`${dish} is ready`), 300);
}
cook('Plov', (first) => {
console.log(first);
cook('Dolma', (second) => {
console.log(second);
cook('Pakhlava', (third) => {
console.log(third);
});
});
});▸ Ожидаемый результат
Plov is ready Dolma is ready Pakhlava is ready
Промисы
Промис (Promise, «обещание») — объект, который представляет результат, готовый в будущем. У него три состояния: pending (ожидание), fulfilled (успешно выполнен) и rejected (завершился ошибкой). При создании промиса мы получаем две функции: resolve для успеха и reject для ошибки. Результат забирают через then, ошибку — через catch, а finally срабатывает в любом случае.
function cook(dish, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!dish) {
reject(new Error('No dish ordered'));
} else {
resolve(`${dish} is ready`);
}
}, time);
});
}
cook('Plov', 300)
.then((message) => {
console.log(message);
return cook('Dolma', 200);
})
.then((message) => console.log(message))
.then(() => cook('', 100))
.catch((error) => console.log('Error:', error.message))
.finally(() => console.log('Kitchen closed'));▸ Ожидаемый результат
Plov is ready Dolma is ready Error: No dish ordered Kitchen closed
then возвращает следующий шаг, а все ошибки ловятся в одном catch.async и await
async/await позволяет писать код с промисами так, будто он обычный синхронный. Функция, объявленная со словом async, всегда возвращает промис. Внутри неё await ждёт результата промиса, но не замораживает остальную программу. Ошибки ловят знакомым try...catch.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function cook(dish, time) {
await wait(time);
if (!dish) throw new Error('No dish ordered');
return `${dish} is ready`;
}
async function dinner() {
try {
console.log(await cook('Plov', 300));
console.log(await cook('Dolma', 200));
console.log(await cook('', 100));
} catch (error) {
console.log('Error:', error.message);
} finally {
console.log('Kitchen closed');
}
}
dinner();▸ Ожидаемый результат
Plov is ready Dolma is ready Error: No dish ordered Kitchen closed
async function getScore() {
return 42;
}
const result = getScore();
console.log(result instanceof Promise);
result.then((score) => console.log('Score:', score));▸ Ожидаемый результат
true Score: 42
Параллельные задачи и fetch
Если задачи не зависят друг от друга, ждать их по очереди — пустая трата времени. Promise.all запускает все промисы сразу и, когда все готовы, возвращает результаты массивом в том же порядке. Три задачи ниже по очереди заняли бы 600 мс, а параллельно — около 300 мс. Если хотя бы один промис завершится ошибкой, Promise.all тоже завершится ошибкой.
const cook = (dish, ms) =>
new Promise((resolve) => setTimeout(() => resolve(`${dish} is ready`), ms));
async function breakfast() {
const results = await Promise.all([
cook('Tea', 300),
cook('Bread', 100),
cook('Jam', 200),
]);
console.log(results);
}
breakfast();▸ Ожидаемый результат
[ 'Tea is ready', 'Bread is ready', 'Jam is ready' ]
В реальных проектах чаще всего встречается загрузка данных с сервера через fetch. fetch возвращает промис; когда приходит ответ, response.json() превращает его в объект JavaScript — и это тоже промис. Пример ниже — шаблон: адрес в нём условный.
async function loadWeather(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`);
if (!response.ok) {
throw new Error(`Server answered ${response.status}`);
}
const data = await response.json();
console.log(`${city}: ${data.temperature} °C`);
} catch (error) {
console.log('Could not load the weather:', error.message);
}
}
loadWeather('Baku');| Способ | Плюс | Минус |
|---|---|---|
| Колбэк | прост, работает везде | нечитаем при вложенности |
| Promise | цепочки, один catch на все ошибки | длинные цепочки тоже читаются тяжело |
| async/await | читается как обычный код, try...catch | легко забыть await |
Напиши асинхронную функцию countdown(from): она выводит числа от from до 1, после каждого ждёт 300 мс через wait(300), а в конце выводит Go!. await в последней строке работает здесь так же, как на верхнем уровне модуля.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function countdown(from) {
// print from, from - 1, ..., 1 with a 300 ms pause after each number
// then print Go!
}
await countdown(3);▸ Ожидаемый результат
3 2 1 Go!
Главное
- Асинхронный код не замораживает программу во время ожидания;
setTimeoutоткладывает функцию на потом. - Готовые колбэки выполняются только после всего синхронного кода (цикл событий).
- У промиса три состояния: pending, fulfilled, rejected; результат берут через
then, ошибку — черезcatch. async-функция всегда возвращает промис;awaitждёт результата, ошибки ловитtry...catch.Promise.allвыполняет независимые задачи параллельно и возвращает результаты в том же порядке.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');?