Промисы стали настоящим спасением человечества и среди прогрессивных разработчиков являются основным способом управления асинхронным кодом.
Полное описание всех возможностей и аспектов поведения промисов является объемной задачей, которая может запутать на первых порах, поэтому в этом уроке мы остановимся на ключевых особенностях поведения. Все остальное можно почерпнуть из стандарта и/или документации.
Знакомству с промисами способствует понимание темы "конечные автоматы".
Начнем по традиции с примера:
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
.
Изначально он находится в состоянии 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
});
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты