Зарегистрируйтесь, чтобы продолжить обучение

Promises JS: Синхронная асинхронность

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

Полное описание всех возможностей и аспектов поведения промисов является объемной задачей, которая может запутать на первых порах, поэтому в этом уроке мы остановимся на ключевых особенностях поведения. Все остальное можно почерпнуть из стандарта и/или документации.

Знакомству с промисами способствует понимание темы "конечные автоматы".

Начнем по традиции с примера:

const file = '/tmp/hello1.txt';
import { writeFile, readFile } from 'fs-promise';

writeFile(file, 'hello world')
  .then(() => readFile(file, 'utf8'))
  .then(contents => console.log(contents))
  .catch(err => console.log(err));
// hello world

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

Абзац выше – это пример того, как выглядит типичная программа, построенная на промисах. Так что такое промис?

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

Интерфейс:

  • Promise.prototype.then(onFulfilled, onRejected)
  • Promise.prototype.catch(onRejected)

Отсутствие callback hell происходит благодаря тому, что мы всегда работаем на уровне последовательных вызовов then, а не уходим в глубину.

Разберем пример выше по косточкам. Первый вызов writeFile(file, 'hello world') возвращает тот самый промис, и пока не важно, как он строится внутри, сейчас мы пытаемся понять то, как с ним работать.

// Вызов ничем не отличается кроме того, что мы не передаем колбек
writeFile(file, 'hello world')

После этого у нас есть два варианта:

  • Мы вызываем then и передаем функцию onFulfilled, которая будет вызвана в случае успешного выполнения асинхронной операции
  • Мы вызываем catch и передаем функцию onRejected, которая будет вызвана, в случае ошибок в результате выполнения асинхронной операции.

Функция onFulfilled принимает на вход данные, которые были получены в результате предыдущего выполнения. Таким образом идет передача данных по цепочке.

.then(() => readFile(file, 'utf8'))
.then(contents => console.log(contents))

Данные, возвращаемые из функции onFulfilled, переходят по цепочке в функцию onFulfilled следующего then. Но если вернуть promise, то в следующем then окажутся данные, полученные в результате выполнения этого промиса, а не сам промис. Что и происходит в примере выше: мы возвращаем readFile(), а ниже получаем contents. То есть, промисы хорошо комбинируются друг с другом.

Конечный автомат

Теперь попробуем посмотреть внутрь промиса. С концептуальной точки зрения промис – это конечный автомат, у которого три состояния: pending, fulfilled, rejected.

Promise states

Изначально он находится в состоянии pending, а дальше может перейти в одно из двух: либо выполнен (fulfilled), либо отклонен (rejected). И все, больше никакие переходы невозможны. Придя один раз в одно из терминальных (конечных) состояний, промис больше не подвержен изменениям, как бы мы не старались снаружи заставить его перейти в другое состояние.

Реализация

const promiseReadFile = filename => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      err ? reject(err) : resolve(data);
    });
  });
};

Любая функция возвращающая промис, внутри себя создает объект промиса привычным способом. Конструктор Promise принимает на вход функцию, внутри которой запускается выполнение асинхронной операции. Делается это, кстати, сразу, промисы не являются примером отложенного (lazy) выполнения кода. Но это еще не все. Промис требует от нас некоторых действий для своей работы. Во входную функцию передаются две другие: reject и resolve. reject должна быть вызвана в случае ошибки с передачей внутрь объекта error, а resolve — в случае успешного завершения асинхронной операции с передачей внутрь данных, если они есть.

Ошибки

Ошибка обрабатывается ближайшим обработчиком onRejected в цепочке вызовов. При этом существует два варианта определения обработчика. Первый - через catch, второй - с помощью передачи в then второго параметра. Это продемонстрировано в примере ниже:


promiseReadFile('file1')
  .then(data => promiseWriteFile('file2', data))
  .then(() => promiseReadFile('file3'))
  .then(data => console.log(data))
  .catch(err => console.log(err));
  // .then(null, err => console.log(err));

Promise.all

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

const readJsonFiles = filenames => {
  // N.B. passing readJSON as a function,
  // not calling it with `()`
  return Promise.all(filenames.map(readJSON));
}

readJsonFiles(['a.json', 'b.json'])
  .then(results => {
    // results is an array of the values
    // stored in a.json and b.json
  });

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

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

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

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

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