Не бойтесь Promise-объектов в JavaScript — они нужны вашему коду

|

Каждый день нам приходится сражаться с асинхронностью Javascript. Еще пару лет назад это была настоящая война. Мы писали множество callback-функций для всех анимаций на странице, простых тайм-аутов, запросов к серверам с “умными” API для получения информации из баз данных. Следующий пример демонстрирует, как можно получить список всех магазинов, где есть ингредиенты для яблочного пирога.

Здесь мы видим четыре callback-функции: две — для получения результата и еще две — для обработки ошибок. Выглядит не очень красиво, правда? Что если бы у нас был более сложный пример с использованием кэша и большого количества модулей?
Все запрашиваемые данные могут использоваться в вашем приложении несколькими модулями или виджетами, и каждый из них будет отправлять запрос на сервер, чтобы получить данные. Конечно, нам также стоит принять во внимание индикаторы процесса загрузки, которые должны отображаться в нужном месте до тех пор, пока мы не получим ответ от сервера. В таком случае, единственным выходом является синхронизация запросов. И вот перед вами герой, который спасет положение — это promise-объект.

promises в javascript

 

Использование Promise-объектов

Узнать о нем можно из множества источников. К примеру, самая базовая спецификация известна под названием CommonJS Promises/A. Существует также ее более расширенная версия — Promises/A+.
Если не вдаваться в подробности, promise-объект содержит в себе текущее состояние запроса. Это означает, что каждый фрагмент кода, который имеет ссылку на этот объект, знает о существовании запроса и его текущем состоянии, каким бы оно ни было: “завершен”, “завершен с ошибкой” или все еще “в ожидании ответа”. Кроме того, мы можем установить обработчики, которые будут вызываться при изменении состояния запроса.
Существуют различные типы promise-объектов: A, B, C, D, KISS. Все они используют одну и ту же концепцию хранения текущего состояния объекта, однако отличаются наборами методов и именами promise-объектов. Тем не менее, все они облегчают нам жизнь, оставляя за кадром всю логику транспортировки, обработки ошибок и сбора результатов и позволяя нам заниматься только бизнес-логикой.
Давайте обратимся к небольшому, но полезному примеру. У нас имеется объект с именем “Connection” для получения данных с сервера. У этого объекта есть метод “request”, который принимает параметры запроса и возвращает promise-объект. Нам нужно дождаться ответа и отобразить индикатор процесса загрузки. В этом примере мы не будем использовать никаких библиотек для создания promise-объектов. Это просто псевдо-код для того, чтобы проиллюстрировать работу с promise-объектами.

Отлично! Мы разделили два разных модуля: модуль, показывающий процесс загрузки, и модуль, анализирующий данные с сервера. Теперь нам не нужно беспокоиться о том, чтобы вызывать метод “stop” для того, чтобы удалить индикатор процесса загрузки из DOM-модели. Мы можем вообще о нем забыть, поскольку он удалится сам. Кроме того, была решена проблема с двумя вложенными callback-функциями. Весь код можно прочитать как текст на английском языке, если дать функциям правильные названия: “Запросить пирог, затем получить ингредиенты, далее сделать запрос по магазинам, и, наконец, показать все открытые магазины”.
На этом самая простая часть использования promise-объектов заканчивается. Давайте теперь добавим кэш и два независимых виджета, которые будут запрашивать практически одну и ту же информацию с сервера. Прежде всего, нам необходимо создать кэш, который всегда будет возвращать promise-объект. Если в кэше уже находится значение, то возвращаемый promise-объект будет иметь состояние “завершен”, и сразу же будет вызвана callback-функция, переданная в метод “then”. Если значения в кэше нет, то запрос для его получения будет отправлен на сервер. Ключом для каждого значения в нашем кэше будет является строка JSON для запроса. Фрагмент кода представлен ниже:

Это простая реализация кэша без методов для его обновления и указания срока хранения данных. Здесь используется только один метод — “get”. Если мы попытаемся получить какое-либо значение из кэша, он вернет promise-объект.
Promise-объект может быть либо завершенным успешно/с ошибкой, либо ждать ответа. Как видите, если мы вызовем метод “get” дважды, мы получим один и тот же promise-объект. Это помогает предотвратить отправку на сервер двух одинаковых запросов. Кроме того, все наши виджеты и модули могут быть независимыми друг от друга. Давайте посмотрим, как мы можем использовать кэш в нашем приложении.
У нас есть два виджета: первый показывает ингредиенты выбранного пирога, а второй — магазины, где мы можем их купить. Это два независимых виджета, которые “не знают” о существовании друг друга. Кроме того, нам нужно применить модуль для визуализации процесса загрузки. Его необходимо немного изменить и добавить еще один аргумент к методу “showUntilRequestsAreFinished”, где будет храниться ссылка на элемент, процесс загрузки которого должен быть показан. Давайте посмотрим на код:

Как видите, все эти модули создают один и тот же запрос, чтобы получить рецепт яблочного пирога по его ID. Но наш “умный” кэш не позволяет повторить это действие, и во второй раз (для recipeWidget, который создается в другом event loop из-за таймаута), он вернет promise-объект, созданный первым вызовом функции “get” (для shopsWidget). Эти два виджета ждут один и тот же ответ с сервера. Когда ответ приходит, shopsWidget первым получает все данные, и только после того, как он запросит информацию о магазинах, recipeWidget получит те же данные.

Реализация promise-объектов

Итак, псевдокод выглядит замечательно, но в нем есть один большой недостаток: он не работает. Существует два способа решить эту проблему:
создать свой собственный cупер-мега-классный Promise-объект или использовать уже существующие библиотеки.
Первый способ отличный, если вы не хотите зависеть от каких-либо сторонних библиотек. Вы можете выбрать любую реализацию, которая вам нравится, и добавить ещё больше методов. Но будьте внимательны, здесь встречается множество подводных камней. Поэтому мы будем использовать второй способ.
Cуществует множество библиотек для создания promise-объектов:

  1. Q
  2. when.js
  3. RSVP.js
  4. Deferred (как часть jQuery или Dojo)
  5. Множество других, таких, как WinJS, и так далее.

Все они поддерживают модель, которая описана в спецификации (но будьте внимательны с jQuery 1.9 и более ранних версий. Ее реализация promise-объектов немного отличается. Метод “then” не возвращает promise-объект, и вы не можете сделать цепочку из всех запросов).
Прежде чем погрузиться в реализацию promise-объектов, я должен объяснить, что представляет собой такое понятие как deferred-объект. Как вы уже поняли, promise-объект представляет текущее состояние какой-либо асинхронной операции. Вы можете прочитать это состояние, но не можете его изменить. Для того, чтобы изменить состояние promise-объекта, необходимо использовать связанный с ним deferred-объект. У всех deferred-объектов есть как минимум два метода (чтобы отклонить или выполнить связанный с ними promise-объект) и одно свойство (ссылка на promise-объект, связанный с данным deferred-объектом). Эти методы часто называются “reject” и “resolve”(“fulfill”). Deferred-объект может также иметь метод “notify”, который показывает прогресс выполнения асинхронной операции связанной с данным promise-объектом.
Конечно, у всех библиотек имеется конструктор для deferred-объектов и обычных promise-объектов, у которых есть метод “then”, и другие, не менее занимательные методы. Давайте ближе познакомимся с их API.

1. Q.js

Это небольшая библиотека для создания promise-объектов (она занимает примерно 2.5 Кб в архиве). Может использоваться с Node.js, а также в браузере. Кроме того, Q может обмениваться promise-объектами с jQuery, Dojo, When.js, WinJS и другими библиотеками. Она создает глобальный объект “Q”, являющийся функцией. Promise-объект можно создать следующим образом: Q (некоторое значение). Q.defer() используется для создания deferred-объекта, который может быть отклонен, разрешен (или выполнен), также через него могут передаваться оповещения (чтобы показать прогресс). Deferred-объект используется для того, чтобы управлять состоянием promise-объекта. Например, нам нужно сделать какое-либо асинхронное действие, вернуть promise-объект и разрешить (или отклонить) его, когда действие будет закончено. Давайте обратимся к еще одной реализации recipeWidget:

Существует также другой способ создания promise-объектов и изменения их состояния. Для этого нужно вызвать метод Promise и передать callback-функцию. Эта callback-функция называется “resolver”, и имеет два аргумента: функции для завершения и отклонения созданного promise-объекта:

Итак, deferred-объект имеет достаточно простой интерфейс. Самой интересной частью данной библиотеки является promise-объект. Он включает в себя множество полезных методов. Приведу самые важные из них:
1) then — такой же, как и в спецификации;
2) inspect — возвращает объект с текущим состоянием promise-объекта и его значением;
3) all — возвращает promise-объект, который будет выполнен, когда все promise-объекты, которые передаются в массиве в качестве аргумента, выполнены;
4) spread — похож на “then”, но “преобразует” массив в callback-функцию с переменным числом аргументов. Это означает, что, если мы применим его к promise-объекту, который мы получили с помощью метода “all”, количество аргументов у callback-функции, переданной первым параметром, будет таким же, как и длина массива, который передается в “all”:

5) методы для проверки состояния promise-объекта: isReject, isPending, isFulfilled;
6) timeout – возвращает promise-объект, который будет отклонен, если он не был завершен за определенный период времени;
7) множество методов, таких как thenResolve, thenReject, которые можно заменить методом “then”.
А теперь посмотрим на реальный пример использования данной библиотеки. В первом примере мы будем последовательно запрашивать три картинки (чтобы загрузка не произошла слишком быстро, добавим некоторую задержку с помощью Fiddler). Каждый запрос будет создавать новый deferred-объект и возвращать promise-объект. Когда картинка загрузится, мы завершим соответствующий promise-объект и создадим следующий запрос. Код представлен ниже:

промисы_1

Конечно, загрузка картинок последовательно не является лучшим решением, поскольку это занимает много времени. Во втором примере мы загрузим все картинки параллельно и покажем их, когда они будут загружены. Для этого применим метод “all”:

промисы_2

2. When.js

Эта библиотека очень похожа на предыдущую. Для создания deferred-объекта нам нужно использовать метод when.defer(). Методы deferred и promise-объектов не отличаются от подобных методов других библиотек. Однако, у этой библиотеки есть несколько отличных методов, которые могут немного облегчить нашу жизнь:
1) Методы массива: “map”, “reduce”, “some” — похожие на обычные методы для массивов;

2) any — этот метод принимает массив promise-объектов и возвращает promise-объект, который будет выполнен после того, как выполнится хотя бы один promise-объект из переданного массива.

3. RSVP.js

Самая маленькая библиотека для создания promise-объектов. Использует тот же способ для создания deferred-объектов. Имеет классическую Promise/A+ реализацию.
Кроме того, она имеет расширенный набор событий и позволяет подписаться или отписаться от некоторых событий, таких как “created”, “chained”, “fulfilled”, “rejected”. Кроме того, библиотека имеет встроенный EventTarget модуль, который может превратить любой объект в обработчик событий, добавив методы для привязки функций к определенным событиям и отправки событий:

Другие способы реализации promise-объектов ничем особенным не отличаются. Обратите внимание, что некоторые из них могут не поддерживать всю функциональность классических promise-объектов (таких как promise-объекты в JQuery 1.9 и более ранних версий).

Будущее promise-объектов

В заключение, несколько слов о браузерной реализации promise-объектов. Да, это наше будущее, и оно не столь отдаленно. Можно создавать нативные promise-объекты в Chrome (начиная с версии 32) и Firefox (с версии 27) уже сейчас. Интерфейс достаточно прост. Существует глобальный конструктор Promise. Нужно вызвать этот конструктор и передать resolver в качестве первого параметра. У созданного promise-объекта есть только два метода на настоящий момент — “then” и “catch” :

Благодарим за внимание и ждем ваших вопросов и комментариев.

The following two tabs change content below.
Владимир Дашукевич

Владимир Дашукевич

Разработчик XB Software c большим опытом в различных областях веб-программирования. Поклонник JavaScript и теории графов. Любит применять новые технологии и непростые алгоритмы в веб.