При написании кода на JavaScript одной из самых запутанных концепций являются колбэки. Промисы — это новый подход, который улучшает работу с асинхронным кодом.
Одной из главных проблем колбэков является цепочка различных асинхронных операций. В результате вы вызываете одну анонимную функцию за другой, передавая значения, что приводит к неуправляемому «аду колбэков». Промисы пытаются решить эту проблему, но поначалу могут вызывать затруднения.
Давайте определим, что такое промисы, почему они невероятно полезны и как выполнять вызовы параллельно или последовательно. Для этого рассмотрим различные примеры выполнения HTTP-запросов с использованием двух разных библиотек Node.js.
Прежде чем начать, вам понадобятся следующие вещи для выполнения наших примеров кода:
— Node.js версии 6.0 или выше. Вы можете проверить свою версию, выполнив команду `node -v` в командной строке. Обновить версию можно, загрузив последнюю сборку с их сайта или используя инструмент, например nvm.
Как только это будет готово, создайте новую папку. Я создам папку `promises` в своей домашней директории. Установите библиотеки `fetch` и `request`, выполнив следующую команду в этой папке:
Создайте новый файл с именем `promises.js` и добавьте в него следующие строки для загрузки библиотеки:
Мы будем работать с этим же файлом `promises.js` на протяжении всей статьи.
Чтобы понять преимущества промисов, давайте сначала посмотрим, как выполнить асинхронный вызов без них. Для этого мы сделаем HTTP GET-запрос с использованием библиотеки `request`.
Добавьте следующие строки в `promises.js`:
Теперь выполните этот код, запустив следующую команду:
Как видите, мы передаем колбэк-функцию вторым аргументом в вызов `request.get`. Библиотека автоматически выполнит эту функцию, когда придет ответ на HTTP-запрос. Она передаст три аргумента. Первый аргумент — это возможная ошибка или `null`, если запрос прошел успешно. Второй аргумент — это HTTP-ответ, а третий — тело ответа.
Если мы используем `fetch` вместо `request.get`, мы можем воспользоваться промисами, так как `fetch` возвращает `Promise` вместо того, чтобы принимать колбэк вторым аргументом. `Promise` — это объект, у которого есть два важных метода: `then()` и `catch()`. Метод `then()` может принимать 1 или 2 аргумента, а `catch()` используется для обработки ошибок.
Для `then()` первый аргумент-функция вызывается, если результат вызова был успешным. Вторая функция будет вызвана, если при разрешении промиса произошла ошибка. Позже мы рассмотрим разницу между этим обработчиком ошибок и `catch()`.
Замените предыдущий код следующим, чтобы начать использовать промисы:
Перезапустите код, снова выполнив команду `node promises.js`.
Пока нет большой разницы с кодом на колбэках, кроме того, что он стал немного чище. Настоящая магия начинается, когда мы хотим манипулировать данными или делать несколько вызовов. Для этого действует общее правило: если функция-обработчик, которую мы передаем в `then()` или `catch()`, возвращает значение или другой промис, цепочка промисов продолжается.
В качестве примера добавьте функцию, которая извлекает статус-код и возвращает его:
Запустите код снова. Вывод в консоли должен быть таким же, но наш код стал более структурированным.
Этот код сначала выполнит HTTP-запрос, затем вызовет функцию `extractStatusCode`, и как только эта функция вернет результат, выполнит нашу анонимную функцию, которая выведет статус-код ответа в консоль.
Теперь, когда мы используем промисы, мы можем столкнуться с проблемой. Весь наш код будет молча завершаться с ошибкой, если мы не обрабатываем ошибки должным образом.
Используйте промисы так, как будто вы оборачиваете весь свой код в блок `try {}`. Ваш код просто молча завершится с ошибкой, если вы явно не перехватите их. Перехват ошибок чрезвычайно важен и не просто «хороший тон».
Чтобы правильно перехватывать ошибки, у нас есть два варианта. Первый способ — передать вторую функцию в наш вызов `then()`.
Внесите следующие изменения в свой код, чтобы протестировать это:
Когда вы запустите этот код, вы увидите, что он попадет в обработчик ошибок, который мы добавили, и выведет соответствующие сообщения на экран:
Однако он не выполняет обработчик `catch`, потому что мы возвращаем значение `null` в обработчике. С этого момента цепочка промисов снова считается успешной, так как ошибка была обработана.
Мы можем убедиться, что это продолжает обрабатываться как ошибка, если «выбросим» ошибку или вернем новый промис с помощью `Promise.reject(error)`:
Теперь, когда мы знаем, как обрабатывать ошибку с помощью `then()`, в чем разница между этим и `catch()`?
Чтобы понять это, давайте снова исправим наш фрагмент с `fetch`, используя корректный URL, и вместо этого сломаем функцию `extractStatusCode`, переопределив `response` значением `undefined` перед доступом к свойству `status`:
Обработчик ошибок в части `then()` не выполняется, потому что этот обработчик предназначен только для предыдущего промиса, а не для обработчика. Однако наш обработчик `catch()` будет выполнен, так как он перехватывает любые ошибки, которые происходят в цепочке.
Вот где проявляется магия промисов. Представьте случай, когда мы хотим отправить несколько HTTP-запросов или выполнить несколько асинхронных вызовов и хотим знать, когда они завершатся.
Конечные точки, которые мы хотим запросить, хранятся в массиве. При использовании колбэков это может превратиться в настоящий хаос. Чтобы справиться с этим, нам приходится использовать счетчики в колбэках, чтобы проверить, завершились ли все вызовы, и другие подобные уловки.
С промисами мы можем просто пройтись по массиву сообщений с помощью метода `map`, вернуть промис в функции `map` и передать полученный массив в встроенную функцию `Promise.all()`. Она вернет новый промис, который разрешится, как только все вызовы завершатся успешно, или отклонится, как только один из них завершится с ошибкой.
Хотя параллельное выполнение — это круто и эффективно, иногда нам приходится делать несколько вызовов последовательно из-за ограничений или зависимостей. Для этого мы также можем использовать промисы.
Цепочка промисов, когда вы знаете все необходимые вызовы, выполняется очень просто. Однако это усложняется, если мы динамически генерируем асинхронные функции, которые нужно выполнить.
Есть способ, как это сделать:
Концепция здесь заключается в том, чтобы связать вызовы в цепочку и выполнить следующий, как только предыдущий разрешится, обернув его в обработчик `then()`. Это тот же подход, который мы использовали бы вручную, если бы знали количество вызовов.
Сейчас мы используем цикл `forEach` для этого. Это работает, но не является самым читаемым решением. Чтобы улучшить это, мы можем использовать метод `reduce` нашего массива.
Измените код соответствующим образом:
Общий подход здесь такой же, как и с циклом `forEach`. Мы указываем начальное значение `Promise.resolve([])` и вызываем метод `reduce` для массива `messages` с функцией, которая принимает два аргумента. Один — это предыдущее возвращаемое значение, а другой — текущее значение массива, к которому мы обращаемся. Таким образом, мы можем «свернуть» массив до одного значения. В нашем случае это будет самый последний промис, который мы затем можем использовать, чтобы узнать, когда все завершится.
Теперь, когда мы знаем, как использовать промисы, у нас есть проблема, которую нужно решить. Что делать с асинхронным кодом, который не поддерживает промисы? Для этого мы можем обернуть функцию в новую функцию и использовать конструктор `new Promise()`. Этот конструктор принимает функцию с двумя аргументами: `resolve` и `reject`. Эти аргументы — функции, которые мы вызываем, когда хотим разрешить или отклонить промис.
Вот пример функции, которая читает файл с диска и возвращает его содержимое в промисе:
Когда мы вызываем `new Promise()` с функцией в качестве аргумента, эта функция будет немедленно выполнена асинхронно. Затем мы выполняем `fs.readFile` с необходимыми аргументами. Как только колбэк вызова `readFile` выполнится, мы проверяем, есть ли ошибка. Если есть ошибка, мы отклоняем промис с соответствующей ошибкой. Если ошибки нет, мы разрешаем промис.
Теперь вы, надеюсь, лучше понимаете промисы и готовы оставить времена старого «ада колбэков» ради более чистого и поддерживаемого кода. А если вы увлеклись, посмотрите, что принесет будущее JavaScript с async/await, чтобы еще больше улучшить асинхронное программирование в JavaScript.
Также обязательно дайте знать, каков ваш опыт работы с промисами и почему вы их любите (или ненавидите). Возможно, у вас даже есть какой-то безумный хак с использованием промисов, которым вы хотите похвастаться? Просто напишите мне:
— Email: dkundel@twilio.com
— Twitter: @dkundel
— GitHub: dkundel
— Twitch (стриминг живого кода): twilio