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

  • Tutorial
Хотите использовать 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.

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

Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 22
  • +4
    Для более простых вещей (тот-же jquery ajax) можно использовать Deferreds (1, 2).
    • +1
      Или вот по-русски Инструмент Deferred Object в jQuery.
    • +2
      А в связке с backbone можно и для сложных использовать :)
      • +4
        knockoutjs еще проще и нагляднее
        • +1
          Так же Sproutcore использует Observable. И там есть удобный Data Store, который всем будет знаком из ExtJs
      • +1
        По моему эта библиотека займет нишу из разработчиков переключившихся с дотнет на джаваскрипт и не более
        • 0
          думаю, не переключившихся с .net на js, а работающих с asp.net, которым необходимы уже знакомые инструменты на клиенте.
          • 0
            не очень представляю работу на asp.net и extjs, например
            • +1
              ну почему же. на asp.net можно предоставлять интерфейсы для доступа к данным в формате xml, json, odata и любом др. в виде сервисов.
              причем, независимо будь то WebForms или MVC, все прекрасно работает с клиентскими UI. лично я использую jQuery UI.
              • 0
                не-не это понятно что все равно что будет генерировать данные для построения, например гридов. Но как приспособить описанный в статье observer к этим таблицам из extjs? Это даже если возможно то точно не имеет много смысла.
                • 0
                  я конечно не работал с extjs, однако одной из полезностей может быть вытягивание данных из одного источника (пусть это грид), и их преобразование в новый массив через оператор select.
                  а еще там есть связка для extjs, и для построения UI-логики приложения вполне подойдет.
                  • 0
                    хорошо, в двух словах: надо создать дата модель а-ля мапинг к ждейсону, надо создать датастор (датасорс) с этой моделью, надо создать грид с этим стором. в эту связку очень тяжело что-то всунуть, практически не возможно.

                    а жуквери юай это просто набор виждетов которые очень слабо связаны, так что с ними можно делать что угодно и чем угодно, тут проблем нет
                    • 0
                      хорошо, уговорили :) пусть extjs и трудно будет связать (а то и некуда), все равно предназначение RxJS это не отменяет.
          • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Вопрос «зачем» не стоит) обстоятельства могут повернуться как угодно. Но если человек привык к чему-то он ищет похожие технологии там где он сейчас, это «принцип экономии мышления Маха-Авенариуса»
          Мне же интересно другое, насколько технология придуманная для одного языка с его парадигмой и шаблонами применима к другому
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Именно. Erik Meijer даже как-то говорил, что RxJS для них референс реализация. Правда че-то заглохла JS-серсия. Даже исходник не дают нормальный (только минифицированный).
          • 0
            Сорри за некропостинг, но… Вы всегда пишете JS код в одну строчку? Совершенно нечитабельно =(
            • +1

              конечно же, нет! прост это форматирование поехало с новыми стилями Хабра.

              • 0
                Это печально… Спасибо за ответ!

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.