Асинхронное программирование в JavaScript: полное руководство для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в асинхронное программирование в JavaScript

JavaScript — это однопоточный язык программирования с неблокирующей моделью ввода-вывода. Это означает, что в один момент времени выполняется только одна операция. Однако, благодаря асинхронному программированию, Node.js может обрабатывать тысячи запросов одновременно без блокировки основного потока. В этой статье мы разберём основные концепции асинхронности: колбэки (callbacks), промисы (Promises) и синтаксис async/await.

Что такое асинхронность и зачем она нужна?

Представьте, что вы заказываете пиццу. Если вы стоите у стойки и ждёте, пока её приготовят — это синхронный код. Всё остальное останавливается. Асинхронный подход — вы оставили заказ и занялись своими делами. Когда пицца готова, вас зовут. JavaScript работает именно так: он не ждёт завершения операции (чтения файла, запроса к серверу), а продолжает выполнять следующий код. Когда операция завершается, вызывается специальная функция — колбэк.

В Node.js асинхронность особенно важна, потому что сервер должен одновременно обслуживать множество клиентов. Без неё каждый новый запрос блокировал бы работу всего приложения.

Синхронный vs Асинхронный код: пример

Рассмотрим простой пример чтения файла. Синхронный вариант блокирует выполнение:

const fs = require('fs');

// Синхронное чтение — блокирует поток
const data = fs.readFileSync('file.txt', 'utf8');
console.log('Данные прочитаны:', data);
console.log('Этот код выполнится только после чтения файла');

Асинхронный вариант не блокирует:

const fs = require('fs');

// Асинхронное чтение — не блокирует
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('Данные прочитаны:', data);
});

console.log('Этот код выполнится сразу, не дожидаясь чтения файла');

Основные способы работы с асинхронностью

В JavaScript (и Node.js) существует три основных подхода к асинхронному программированию. Мы рассмотрим их от самых старых к современным.

1. Callback (функции обратного вызова)

Колбэк — это функция, которая передаётся в другую функцию и вызывается после завершения асинхронной операции. Это самый базовый и старый метод.

function getUserData(userId, callback) {
  setTimeout(() => {
    const user = { id: userId, name: 'Анна' };
    callback(null, user); // первый аргумент — ошибка (если есть)
  }, 1000);
}

getUserData(1, (err, user) => {
  if (err) {
    console.error('Ошибка:', err);
    return;
  }
  console.log('Пользователь:', user.name);
});

Проблема: когда операций становится много, код превращается в "ад колбэков" (callback hell) — глубоко вложенные функции, которые сложно читать и отлаживать.

2. Promise (Промисы)

Promise — это объект, который представляет собой результат асинхронной операции. Он может находиться в одном из трёх состояний:

  • pending (ожидание) — операция ещё не завершена
  • fulfilled (выполнено) — операция завершена успешно
  • rejected (отклонено) — операция завершена с ошибкой

Промисы решают проблему callback hell и делают код более линейным:

function getUserData(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const user = { id: userId, name: 'Иван' };
      // resolve — успех, reject — ошибка
      resolve(user);
      // reject(new Error('Пользователь не найден'));
    }, 1000);
  });
}

// Использование цепочки .then()
getUserData(1)
  .then(user => {
    console.log('Пользователь:', user.name);
    return user.id;
  })
  .then(id => console.log('ID:', id))
  .catch(err => console.error('Ошибка:', err))
  .finally(() => console.log('Операция завершена'));

Метод Promise.all() позволяет запустить несколько промисов параллельно:

const promise1 = getUserData(1);
const promise2 = getUserData(2);

Promise.all([promise1, promise2])
  .then(users => {
    console.log('Оба пользователя загружены:', users);
  });

3. Async / Await

Синтаксис async/await — это "синтаксический сахар" над промисами, который делает асинхронный код похожим на обычный синхронный. С ним код становится ещё более читаемым.

Ключевое слово async ставится перед функцией, а await — перед промисом, который нужно дождаться:

async function loadUser() {
  try {
    const user = await getUserData(1);
    console.log('Пользователь:', user.name);
    
    const user2 = await getUserData(2);
    console.log('Второй пользователь:', user2.name);
    
    return user;
  } catch (error) {
    console.error('Ошибка загрузки:', error);
  }
}

// Вызов асинхронной функции
loadUser().then(result => console.log('Готово:', result));

Обратите внимание: await можно использовать только внутри функций, объявленных с async. Ошибки обрабатываются через обычный try/catch.

Event Loop: как это работает под капотом

Чтобы понимать асинхронность, нужно знать о Event Loop (цикле событий). Это механизм, который позволяет Node.js выполнять неблокирующие операции, несмотря на однопоточность.

Упрощённая схема работы:

  1. Выполняется синхронный код (стек вызовов).
  2. Асинхронные операции (setTimeout, запрос к БД, чтение файла) передаются в Web API (или libuv в Node.js).
  3. Когда операция завершается, её колбэк попадает в очередь задач (task queue).
  4. Event Loop проверяет стек вызовов: если он пуст, берёт первую задачу из очереди и помещает в стек.

Пример, который часто путает новичков:

console.log                

Блоги

Рекомендаций по книгам