Как стать автором
Обновить

Reactive Extensions for JavaScript. Полное руководство

Время на прочтение 7 мин
Количество просмотров 41K
Хотите использовать Observable Collections? Слышали про Reactive Extensions? Нравится LINQ? Не нравится писать спагетти-код? Нужны монады? И все это на JavaScript?

Итак, что же представляет из себя Rx for JavaScript?
Rx расшифровывается как Reactive Extensions. Как гласит определение на странице проекта:
Rx – библиотека для создания асинхронных и событийно-ориентированных программ с помощью коллекций, следуя паттерну «Наблюдатель».

Основная идея – рассматривать асинхронные вычисления как источники данных. Например, компьютерная мышь – есть не что иное, как источник кликов и перемещений курсора. Для работы с таким набором данных служат Observable Collections. Так как RxJS берет корни из мира .NET, следовательно, нам становятся доступны некоторые возможности LINQ (операторы select, where, take, skip и т.д.). Об этом чуть позже, а пока рассмотрим структуру библиотеки.

Интеграция с существующими фреймворками

Для начала загрузим RxJS по следующей ссылке. В комплекте идут расширения для работы с jQuery, MooTools, prototype, Dojo, ExtJS, а также с популярными сервисами типа Google Maps. Сама же библиотека состоит из одного файла rx.js размером 7,4 Кбайт (в несжатом виде 30 Кбайт).



В примерах в качестве редактора будет использоваться Visual Studio 2010 с установленным пакетом Web Developer. Клиентский фреймворк – jQuery.

Итак, создадим простую HTML-страницу и подключим RxJS.

<head>
   <title>Samplestitle>
   <script type="text/javascript" src="Scripts\rx.js"></script>
<head>

После этого IntelliSense начнет показывать методы Rx.



Немного теории

Прежде чем двигаться вперед, хотелось бы пояснить некоторые термины и соглашения, принятые в Rx.
  • Observable – коллекция, источник данных, который генерирует события и отправляет их подписчикам.
  • Observer («Наблюдатель») – подписчик, который и будет обрабатывать сигналы от источника.
  • Именно Observable подписывает на события Observer, но не иначе.
  • Основа Rx – теория монад, пусть и нереализованных в классическом виде. Их присутствие выражается в «ленивых» (отложенных) вычислениях, а также в возможности отменять некоторые из них. На этом пункте мы остановимся подробнее чуть позже в статье.
  • «Холодный» обозреватель (Cold Observable) – источник, отсылающий новые данные подписчикам только после вызова метода Subscribe.
  • «Горячий» обозреватель (Hot Observable) – источник, отсылающий данные даже при отсутствии подписчиков. После вызова Subscribe клиенты начинают получать данные не с начала генерации, а лишь с текущего момента. Примером могут служить события DOM-элементов.
  • Названия методов RxJS следуют правилу CamelCase.

Observers

Как говорилось выше, для подписки на события Observable используется метод Subscribe:

var source = null; var subscription = source.Subscribe( function (next) { console.log("OnNext: " + next); }, function (exn) { console.log("OnError: " + exn); }, function () { console.log("OnCompleted"); });

и

var source = Rx.Observable.Empty(); var observer = Rx.Observer.Create( function (data) { $("
").text(data).appendTo("#content"); }); var handle = source.Subscribe(observer);

Рассмотрим метод Subscribe подробнее:
  • Возвращает ссылку (handle) на созданный/переданный Observer
  • В качестве первого оверлоада принимает до трех параметров: обработчик для каждого элемента массива данных (OnNext); обработчик исключений (OnError); обработчик завершения (OnCompleted)
  • Во втором оверлоаде передается уже созданный Observer с возможностью управления событий OnNext, OnError, OnCompleted.



Чтобы отписать (unsubscribe) наблюдателя не существует отдельного метода. Вместо этого в RxJS из среды .NET пришел паттерн Dispose.
Так ссылка (handle) для Observer содержит метод Dispose, вызвав который можно уничтожить объект.



Observables

В большинстве случаев Вы всегда будете работать с «горячими» обозревателями: будь то вызов веб-сервиса, либо работа с DOM-элементами. «Холодные» обозреватели в реальных приложениях встречаются не так часто, однако при юнит-тестировании становится ясно их предназначение – работа с заранее подготовленными набором данных.
Ниже приведена таблица API для создания Observable и их описанием.
Название метода Описание
 Empty()   Возвращает пустую коллекцию с вызовом OnCompleted
 Throw(error_msg)   Возвращает пустую коллекцию с вызовом OnError
 Return(value)   Возвращает коллекцию из одного элемента
 Range(start, count)   Возвращает массив 32-битных Integer
 Generate(initial_state, condition, selector, iterate)  Возвращает коллекцию, созданную следуя параметрам
 GenerateWithTime(time)   Аналогичен методу Generate, однако добавляет промежуток времени между генерацией элементов
 Never()   Возвращает пустую коллекцию без вызова событий
 toObservable(event_name)  Расширение для работы с объектом jQuery
 FromArray()  Инициализирует коллекцию из JS-массива
 Create(subscribe_detail)  Создает коллекцию из деталей подписки наблюдателя
 FromDOMEvent(dom_element, event_name)  Расширение для работы с DOM-элементами
 FromIEEvent(dom_element, event_name)  Расширение для работы со специфичными событиями DOM-элементов Internet Explorer’a
 FromHtmlEvent(dom_element, event_name)  Является обобщенным методом для FromDOMEvent() и FromIEEvent()

LINQ

Итак, мы подошли к одной из самых интересных возможностей RxJS – поддержке LINQ. Думаю, многие слышали об этой замечательной технологии, доступной на платформе .NET.
LINQ расшифровывается как Language Integrated Query, позволяя выполнять запросы к различным коллекциям. С появлением RxJS в мир JavaScript пришли не все методы, но самые полезные:
  • Select (SelectMany)
  • Take (TakeUntil, TakeWhile, TakeLast)
  • Where
  • Skip (SkipUntil, SkipWhile, SkipLast)
  • GroupBy
  • И т.д.

Добавились и новые, присутствующие только здесь:
  • DistinctUntilChanged – производит мониторинг состояния коллекции. События OnNext, OnCompleted будут вызваны только при изменении данных.
  • Do – предоставляет доступ к новым данным до вызова DistinctUntilChanged
  • Throttle – задает минимальное время, через которое будет произведен трекинг коллекции на предмет новых данных
  • Timestamp (RemoveTimestamp) – вместо передачи управления данными оператору, передается timestamp, после обработки которого вызывается следующий оператор

Полный список приводить не буду, т.к. вы сами сможете просмотреть остальные методы.
Для более ясного понимания картины ниже приведена схема работы подписки на события:



Так как в JavaScript не существуют лямбда-выражений, их роль в качестве параметров для LINQ-методов выполняют обычные функции:

var items = [1, 2, 3, 4, 5]; var observable = Rx.Observable.FromArray(items).Select(function (item) { return { Value: item, Sqrt: Math.sqrt(item) }; }).Where(function (item) { return item.Value + 2 < 5; });

Возвращаясь к новым функциям (DistinctUntilChanged, Throttle и т.д.) хотелось бы отметить их чрезвычайную полезность. Так Throttle заменит вам использование setInterval и setTimeout в некоторых ситуациях, а его использование вместе с DistinctUntilChanged позволит сократить, например, количество ajax-запросов при вводе текста.
И наглядный код для примера:

var input = $("#textbox").toObservable("keyup") .Select(function (event) { return $(event.target).val(); }) .Timestamp() .Do(function (inp) { var text = "I: " + inp.Timestamp + "-" + inp.Value; $("
").text(text).appendTo("#content"); }) .RemoveTimestamp() .Throttle(1000) .Timestamp() .Do(function (inp) { var text = "T: " + inp.Timestamp + "-" + inp.Value; $("
").text(text).appendTo("#content"); }) .RemoveTimestamp() .DistinctUntilChanged(); var subscribtion = input.Subscribe(function (data) { $("
").text("Your text: " + data).appendTo("#content"); });

Таким образом, до вызова DistinctUntilChanged и передачи массива значений текстового поля, мы производим своего рода логирование.

Скажи нет «спагетти-коду»!

В этой последней части статьи хотелось бы рассмотреть работу с веб-сервисами, которые несколько раз уже упоминались.
Сколько раз вам приходилось писать примерно такой код:

$.ajax({ url: "your_url", dataType: "json", success: function (data, textStatus, xhr) { $("#results").empty(); $.each(data[1], function (_, result) { $("#results").append("
" + result + "
"); }); }, error: function (xhr, textStatus, errorThrown) { $("#error").text(errorThrown); } });

Думаю, довольно часто. В чем его проблема? Правильно, логика обработки данных инкапсулирована вместе с определением источника данных, что превращается в «спагетти-код».
На помощь в решении данной проблемы приходит RxJS. Для начала необходимо подключить файл rx.jQuery, являющийся расширением для интеграции с jQuery.
После этого произведем небольшой рефакторинг вышеприведенного кода:

function serviceCall(text) { return $.ajaxAsObservable( { url: "your_url", dataType: "json", format: "json", data: text }); } var source = serviceCall("search me"); var handle = source.Subscribe(function (result) { $("#results").append("
" + result + "
"); }, function (exn) { $("#error").text(exn); });

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

Послесловие

В этой статье хотелось показать силу библиотеки Reactive Extensions for JavaScript. Сфера ее применения довольно обширна и определяется лишь потребностями самого программиста. По своему опыту могу сказать, что Observable Collections и философия самого RxJS требуют пересмотра своих уже устоявшихся практик написания кода, но оно этого стоит.
Не были рассмотрены юнит-тестирование, отмена операций и оператор SelectMany (именно здесь и появляются монады), но об этом уже в следующей статье.

P.S.

Список полезных ссылок, рассматривающих некоторые моменты более подробно:

Теги:
Хабы:
+33
Комментарии 22
Комментарии Комментарии 22

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн