Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Промисы (Promise) JS: Асинхронное программирование

При всех его преимуществах, асинхронный код очень сложен в анализе. Буквально несколько вложенных колбеков с параллельным выполнением операций — и все, уже практически невозможно разобраться в происходящем. Страшно представить себе большую программу, в которой асинхронно все. Тысяча-другая строк кода — и ни один человек не сможет понять его.

С самого начала разработчики понимали ограниченность подхода с колбеками, но понадобилось немало времени до того, как в JavaScript появилась альтернатива – Promises. Промисы меняют способ организации кода, не добавляя нового синтаксиса. При правильном использовании они позволяют "выпрямить" асинхронный код и сделать его предельно плоским и последовательным.

Большая часть современного JavaScript кода пишется на промисах, а колбеки уходят в прошлое. Например, разработчики Node.js внедрили промисы практически во все встроенные модули. Функции для работы с файловой системой, построенные на промисах, доступны через свойство promises модуля fs. Сравните примеры:

import fs from 'fs';

// Код на колбеках похож на лесенку

fs.readFile('./first', 'utf-8', (_error1, data1) => {
  console.log(data1);
  fs.readFile('./second', 'utf-8', (_error2, data2) => {
    console.log(data2);
    fs.readFile('./third', 'utf-8', (_error3, data3) => {
      console.log(data3);
    });
  });
});

// Код на промисах практически плоский

// Переименование свойства promises в fsp для краткости
const { promises: fsp } = fs;

fsp.readFile('./first', 'utf-8')
  .then((data1) => console.log(data1))
  .then(() => fsp.readFile('./second', 'utf-8'))
  .then((data2) => console.log(data2))
  .then(() => fsp.readFile('./third', 'utf-8'))
  .then((data3) => console.log(data3));

Технически промис — это специальный объект, который отслеживает асинхронную операцию и хранит внутри себя её результат. Он возвращается всеми асинхронными функциями, построенными на промисах.

const promise = fsp.readFile(src, 'utf-8');

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

const promise = fsp.readFile(src, 'utf-8');
// Файл еще не прочитан
console.log(promise);
// Promise { <pending> }
// pending – это состояние промиса говорит о том, что операция еще в процессе

Как получить результат выполнения асинхронной операции? Снаружи — никак, это просто невозможно. Но промис можно "продолжить", используя метод then(), в который нужно передать колбек-функцию. Параметром этого колбека и будет тот самый результат асинхронной операции

// Результат чтения файла передан в колбек-функцию, переданную в then
// колбек вызовется только тогда, когда выполнится чтение файла
fsp.readFile(src, 'utf-8').then((content) => console.log(content));

Колбек именно передается внутрь then(), а не вызывается. Вызов делает уже сам промис тогда, когда выполнится асинхронная операция.

Независимо от содержимого колбек-функции, вызов then() всегда возвращает новый промис. А возврат колбек-функции становится доступным как параметр колбека следующего then(). Именно такая организация промисов позволяет строить цепочки без необходимости вкладывать вызовы друг в друга, тем самым избегая Callback Hell.

// Предположим, что внутри файла был текст Hexlet
const promise = fsp.readFile(src, 'utf-8') // результат цепочки ВСЕГДА промис
  .then((content) => `go to the next then with ${content}`) // игнорируем результат операции
  .then((text) => console.log(text)); // в этот колбек, роль которого играет лог, передается значение с предыдущего then
// => go to the next then with Hexlet
// Вопрос на самопроверку.
// Что выведется на экран, если добавить к промису выше then(console.log)?

А что, если колбек-функция вернет не просто значение, а промис? Тогда параметром следующего then() становится результат выполнения этого промиса. Иначе промисы были бы бессмысленны. Пример:

const promise = /* тут какая-то операция */
  // Из колбека возвращается промис
  .then(() => fsp.readFile(filePath))
  // В колбек попадает результат выполнения предыдущего промиса
  .then((text) => console.log(text));

Использование промисов внутри любой функции автоматически делает эту функцию асинхронной. Она больше не может вызываться как обычная синхронная функция, так как в таком случае невозможно воспользоваться результатом ее работы, дождаться выполнения операции или узнать об ошибках:

// Неправильное определение

export const copy = (src, dest) => {
  fsp.readFile(src, 'utf-8')
    .then((content) => fsp.writeFile(dest, content));
};

// Использование

// делаем что-то синхронное

copy(src, dest);

// делаем что-то еще

Как только в коде появляется асинхронность, код должен менять свою структуру. В случае колбеков он становится вложенным, в случае промисов весь код превращается в непрерывную цепочку промисов.

// Правильное определение

export const copy = (src, dest) => {
  return fsp.readFile(src, 'utf-8')
    .then((content) => fsp.writeFile(dest, content));
};

// Использование

// делаем что-то синхронное

copy(src, dest).then(() => {
  // делаем что-то еще
}).then(/* продолжаем */)
  .then(/* продолжаем */);

Неправильное использование промисов

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


fsp.readFile('./first', 'utf-8')
  .then((data1) => {
    console.log(data1);
    // Читаем файл и продолжаем промис от этой внутренней функции
    return fsp.readFile('./second', 'utf-8').then((data2) => {
      console.log(data2);
      // Читаем файл и продолжаем промис от этой внутренней функции
      return fsp.readFile('./third', 'utf-8').then((data3) => {
        console.log(data3);
      });
    });
  });

Несмотря на то, что здесь используются промисы, код выглядит даже сложнее чем с колбеками. Проблема в том, как построены вызовы. Продолжение цепочки идет не от верхнего промиса, а от каждой последующей асинхронной операции. В теории промисы действительно бывают вложенными, но только там, где по-другому никак. В любой другой ситуации код должен быть плоским и простым:

// Плоская цепочка промисов
fsp.readFile('./first', 'utf-8')
  .then((data1) => console.log(data1))
  .then(() => fsp.readFile('./second', 'utf-8'))
  .then((data2) => console.log(data2))
  .then(() => fsp.readFile('./third', 'utf-8'))
  .then((data3) => console.log(data3));

Дополнительные материалы

  1. promisejs.org

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
с нуля
Разработка фронтенд-компонентов для веб-приложений
1 декабря 10 месяцев
Иконка программы Онлайн-буткемп. Фронтенд-разработчик
Профессия
Новый с нуля
Интенсивное обучение профессии в режиме полного дня
15 декабря 4 месяца
Иконка программы Node.js-разработчик
Профессия
с нуля
Разработка бэкенд-компонентов для веб-приложений
1 декабря 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
с нуля
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
1 декабря 16 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»