Перейти к содержанию
Educora
Продвинутый20 мин12 / 27

Асинхронный JavaScript: колбэки, промисы, async/await

Научись выполнять действия с ожиданием, не замораживая программу: `setTimeout`, колбэки, промисы и `async`/`await`.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять разницу между синхронным и асинхронным кодом и работу setTimeout
  • Создавать промис и обрабатывать результат через then, catch и finally
  • Писать понятный асинхронный код с async/await и try...catch
  • Выполнять несколько задач параллельно с Promise.all

В кафе официант передаёт заказ на кухню, но не стоит у её двери, пока готовится плов, — он обслуживает другие столики. А когда блюдо готово, приносит его. JavaScript работает так же: ожидая медленные операции — ответ сервера, таймер, файл, — он не замораживает страницу, а занимается другими делами. Это называется асинхронным программированием.

Синхронно и асинхронно

Код, который мы писали до сих пор, был синхронным: каждая инструкция ждёт окончания предыдущей. А setTimeout(функция, мс) «откладывает» функцию, чтобы выполнить её через указанное число миллисекунд, и программа сразу переходит к следующей строке. Обрати внимание на порядок вывода:

JavaScript
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
Определение
Цикл событий (event loop)

JavaScript одновременно делает только одно дело. Готовые колбэки ждут в очереди, а цикл событий берёт их оттуда по одному, когда текущий синхронный код закончился. Поэтому даже setTimeout с задержкой 0 мс срабатывает после всего синхронного кода.

Колбэки и их проблема

Самый старый способ получить результат асинхронной работы — передать колбэк: «вызови эту функцию, когда будет готово». Если несколько задач должны идти одна за другой, колбэки вкладываются друг в друга, и код растёт вправо «лесенкой». Программисты называют это callback hell («ад колбэков»).

JavaScript
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 срабатывает в любом случае.

JavaScript
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.

JavaScript
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
Результат тот же, что и с цепочкой промисов, но код читается гораздо легче.
JavaScript
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 тоже завершится ошибкой.

JavaScript
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 — и это тоже промис. Пример ниже — шаблон: адрес в нём условный.

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 в последней строке работает здесь так же, как на верхнем уровне модуля.

Задание · JavaScript
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.

1 / 10
В каком порядке выведет console.log('A'); setTimeout(() => console.log('B'), 0); console.log('C');?