JavaScript async/await

Прежде чем объяснять async/await, нам нужно понять концепции асинхронного программирования в JavaScript; вы можете обратиться к:Асинхронное программирование в JavaScript。

JavaScript — однопоточный язык, то есть он может выполнять только одну задачу за раз. Чтобы избежать блокировки главного потока длительными задачами, JavaScript использует модель асинхронного программирования.

Асинхронное против синхронного

  • Синхронное программирование: Код выполняется последовательно, и следующая операция запускается только после завершения предыдущей.
  • Асинхронное программирование: Некоторые операции помещаются в "очередь задач". Главный поток продолжает выполнять последующий код и обрабатывает задачи в очереди, когда главный поток свободен.

Примеры

// Пример синхронного выполнения
console.log('1');
console.log('2');
// Вывод: 1, 2

// Пример асинхронного выполнения
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// Вывод: 1, 3, 2

Проблема с функциями обратного вызова

До async/await в JavaScript в основном использовались функции обратного вызова для обработки асинхронных операций, но это могло привести к «аду обратных вызовов».

Примеры

getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

Эта вложенная структура затрудняет чтение и поддержку кода.


Введение в Promise

ES6 представил объект Promise для решения проблемы ада обратных вызовов.

Примеры

function getData() {
  return new Promise((resolve, reject) => {
    // Асинхронная операция
    setTimeout(() => resolve('data'), 1000);
  });
}

getData()
  .then(data => {
    console.log(data);
    return getMoreData(data);
  })
  .then(moreData => {
    console.log(moreData);
  })
  .catch(error => {
    console.error(error);
  });

Хотя Promise улучшил проблему колбэков, цепочечные вызовы then() всё ещё недостаточно интуитивны.


Синтаксис async/await

ES2017 ввёл async/await, который основан на Promise и заставляет асинхронный код выглядеть как синхронный.

async функции

Добавьте ключевое слово async перед объявлением функции, чтобы указать, что функция является асинхронной:

async function fetchData() {
  // Тело функции
}

async функции всегда возвращают Promise:

  • Если возвращаемое значение не является Promise, оно автоматически оборачивается в разрешённый Promise
  • Если выбрасывается исключение, оно возвращает отклонённый Promise

await выражения

await можно использовать только внутри асинхронных функций:

async function fetchData() {
  const result = await somePromise;
  console.log(result);
}

await приостанавливает выполнение асинхронной функции до тех пор, пока Promise не завершится:

  • Если Promise разрешён, он возвращает разрешённое значение
  • Если Promise отклонён, он выбрасывает ошибку (которую можно перехватить с помощью try/catch)

Примеры практического применения

Базовое использование

Примеры

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function showMessage() {
  console.log('Старт');
  await delay(1000);
  console.log('Через 1 секунду');
  await delay(1000);
  console.log('Ещё через 1 секунду');
}

showMessage();

Обработка ошибок

Примеры

async function fetchUserData() {
  try {
    const response = await fetch('https://api.example.com/user');
    if (!response.ok) {
      throw new Error('Сетевой ответ не ok');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Не удалось получить данные:', error);
  }
}

Параллельное выполнение

Если вам нужно выполнить несколько асинхронных операций параллельно, вы можете использовать Promise.all:

Примеры

async function fetchMultipleData() {
  const [userData, productData] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/products')
  ]);
 
  const user = await userData.json();
  const products = await productData.json();
 
  return { user, products };
}

Распространённые проблемы и лучшие практики

1. Не забывайте await

Примеры

// Неправильный пример - забыли await
async function example() {
  const data = fetch('/api'); // Пропущен await
  console.log(data); // Выводит объект Promise
}

// Правильный пример
async function example() {
  const data = await fetch('/api');
  console.log(data); // Выводит фактические данные
}

2. Избегайте ненужного async

Примеры

// Не нужно — внутри функции нет await
async function unnecessaryAsync() {
  return 42;
}

// Более простой способ записать это
function simpleFunction() {
  return 42;
}

3. await верхнего уровня

await можно использовать напрямую на верхнем уровне модуля (возможность ES2022):

Примеры

// В модуле
const data = await fetch('/api');
console.log(data);

4. Вопросы производительности

  • Последовательное выполнение против параллельного: используйте Promise.all должным образом для повышения производительности
  • Обработка ошибок: убедитесь, что перехвачены все возможные ошибки

async/await против традиционного Promise

Характеристика async/await Promise then/catch
Читаемость Высокая, как в синхронном коде Средняя, цепочки вызовов
Обработка ошибок Использует try/catch Использует .catch()
Отладка Проще, с понятным стеком вызовов Сложнее, стек вызовов может быть неясным
Структура кода Более плоская Вложенная или с цепочками

Итог

async/await — значительное улучшение в асинхронном программировании на JavaScript. Он:

  1. Делает асинхронный код более читаемым и поддерживаемым
  2. Основан на Promise и совместим с существующим кодом на Promise
  3. Предоставляет более интуитивный способ обработки ошибок
  4. Улучшает процесс отладки

Хотя async/await не полностью заменяет Promise, в большинстве случаев он предоставляет более элегантное решение. Понимание того, как работает async/await, и его лучших практик значительно улучшит ваши навыки асинхронного программирования на JavaScript.

Другие расширения