Потому что React не владеет реактивностью во внешнем интерфейсе

Помимо React и Vue, Angular считается одним из лучших фреймворков JS UI. Однако он отличается парой факторов. В отличие от двух других топ-2, это полноценный фреймворк, включающий все, что вам нужно от фреймворка. Также он имеет уникальную архитектуру и идеологию. Это можно увидеть в том, что его основным языком является TypeScript вместо JavaScript, в нем интенсивно используются декораторы, а также в нем заметно влияние парадигмы функционального реактивного программирования и принятия многих из ее шаблонов.

Функциональное реактивное программирование

Функциональное реактивное программирование (FRP) фокусируется на концепции потоков и на том, как их можно использовать для обработки синхронных и асинхронных данных для создания приложений декларативным образом. Это комбинация реактивного программирования, которое фокусируется на управлении изменениями во времени и асинхронных потоках данных, и функционального программирования, которое предоставляет способы управления этим.

В Angular FRP используется в основном через Observables. Они, в свою очередь, создают интерфейс для обработки асинхронных операций, таких как запросы AJAX или события пользовательского ввода. Все это и многое другое реализовано с помощью библиотеки RxJS.

Что такое RxJS?

Библиотека RxJS (Reactive Extensions for JavaScript) предоставляет реализацию Observables для JavaScript, наряду с другими вспомогательными типами и операторами для работы с ними. Все эти функции составляют надежный инструментарий для функциональной и реактивной работы с асинхронными операциями.

В целом, это библиотека, к которой можно обратиться всякий раз, когда вы имеете дело с FRP на JS или любом другом языке, поддерживаемом проектом ReactiveX. Вот почему Angular и многие другие проекты выбирают его в качестве наблюдаемой реализации и формируют основу для чистой кодовой базы FRP.

Наблюдаемые

Чтобы правильно понять RxJS и FRP в целом, мы должны начать с концепции наблюдаемых.

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

Наблюдаемые объекты играют роль функциональных оберток вокруг таких потоков. Они предоставляют вам способ подписаться на поток и отказаться от подписки (для прослушивания входящих значений) и API-интерфейсы для преобразования входящих данных в соответствии с вашими потребностями.

Создание наблюдаемых

При создании наблюдаемого важно помнить, что по своей сути это просто функция, связывающая наблюдателя (отслеживающего изменения) с производителем (получающим значения).

Посмотрите, как мы можем создать базовую наблюдаемую из интервала, который будет выдавать увеличивающиеся значения каждые 1 с:

Вы можете видеть, что наша наблюдаемая создается с помощью интерфейса Observable из RxJS. Внутри функции мы получаем доступ к подписчику и контролируем поток уведомлений с помощью таких методов, как next(). Возвращенный обратный вызов предназначен для удаления наблюдаемого - идеальное место для очистки рабочего интервала.

Все, что присваивается переменной, оканчивающейся на $ - не требование, а общее соглашение об именах для наблюдаемых.

Функции создания

Код уже довольно ясен, но зачем писать так много, если мы можем просто использовать одну из функций создания RxJS, чтобы сделать это быстрее? Проверить это:

Коротко и просто. RxJS полон таких функций. Позвольте мне показать вам несколько примеров:

Используя функцию from, мы можем создать наблюдаемое из значения, подобного массиву, итерируемого, наблюдаемого или обещанного.

of кажется довольно похожим, хотя и немного другим и более ограниченным. Это также позволит вам создать наблюдаемое из произвольных значений, но на этот раз каждый аргумент станет отдельным элементом в результирующей последовательности.

Таким образом, of([1,2,3]) приведет к наблюдаемой последовательности из 1 элемента ([1,2,3]), а from([1,2,3]) приведет к последовательности из 3 (1, 2, 3). Имейте в виду, что то же самое относится и к строкам типа Array (например, ”test” vs. ”t”, ”e”, ”s”, ”t”).

fromEvent() приближается к пользовательскому интерфейсу, поскольку создает наблюдаемое из событий пользовательского интерфейса. Дайте цели название события, и готово!

И последнее, но не менее важное: fromFetch() предоставляет ярлык для создания наблюдаемого напрямую из fetch(). Это проще и чище, чем использовать from(fetch()).

RxJS полон таких функций, как from(), of() или fromFetch(). Он предоставляет вам несколько способов сделать одно и то же, каждый из которых подходит для разных сценариев. Это как Lodash для наблюдаемых - очень полезно, с множеством вариантов выбора.

Но если наблюдаемый объект - это просто производитель привязки функции к наблюдателю, тогда ни один из уже созданных наблюдаемых объектов ничего не делает. Мы видим производителя и упаковку, но без наблюдателя это бесполезно. Давайте посмотрим, как мы можем это исправить, попутно узнавая о наблюдателях и подписках.

Подписки

Вернемся к нашему простому interval() примеру и посмотрим, как мы можем подписаться на него, чтобы получать обновления.

Как видите, подписаться на наблюдаемое действительно просто. Просто передайте своего наблюдателя вызову subscribe(), и готово! Вызов вернет объект подписки, чтобы представить выполнение наблюдаемого и управлять подпиской.

Наблюдатели

Теперь важно отметить, что наблюдатель - это объект (набор обратных вызовов), а не отдельная функция. Приведенный выше фрагмент представляет собой общий ярлык для обработки только одного типа уведомлений (next) вместо них всех.

Ни один из упомянутых обратных вызовов не требуется, хотя вам наверняка понадобится хотя бы один.

Кроме того, имена обратных вызовов (next, error и complete) эквивалентны методам, которые вы используете для управления потоком уведомлений с наблюдаемой стороны. В нашем настраиваемом интервале наблюдаемого мы использовали только next(), но error() и complete() также доступны наряду с другими методами.

Отписаться

Возвращенный объект подписки (результат subscribe() вызова) удобен для управления подпиской и, что наиболее важно, для отмены подписки на нее.

Имея доступ к объекту подписки, мы можем отказаться от подписки с помощью простого unsubscribe() вызова.

Отказ от подписки важен, поскольку он управляет правильным выполнением наблюдаемых и вызовов функций удаления для предотвращения утечек памяти. Поэтому, когда вы закончите использовать подписку, не забудьте позвонить по номеру unsubscribe().

Множественные подписки

Еще одна вещь, связанная с подписками, которую стоит обсудить: что происходит, когда вы подписываетесь на одно и то же наблюдаемое более одного раза?

Обратите внимание, что у нас разные значения из обеих подписок, даже если мы подписались на одно и то же наблюдаемое. Оба они имеют отдельные интервалы, которые можно увидеть на выходе - особенно с добавленным вызовом setTimeout().

То, что вы видите, является результатом того, что наша наблюдаемая холодная - давайте обсудим, что это означает.

Горячий или холодный

Чтобы определить, является ли наблюдаемое «горячим» или «холодным», мы должны посмотреть, как он обращается со своим производителем.

Являясь источником данных наблюдаемого, производитель может быть создан либо «внутри», либо «вне» наблюдаемого.

Вы можете ясно увидеть это в нашем примере создания наблюдаемого интервала с нуля.

Здесь вы можете представить производителя как комбинацию интервала и переменной count. Он явно создан внутри функции «чертежа» нашего наблюдаемого, делая наблюдаемое холодным.

Это дает несколько интересных свойств:

  • Для каждой подписки создается новый производитель;
  • Для новой подписки будет возвращена такая же последовательность значений;
  • Значения начинают отправляться только после первой подписки (поскольку до этого производителя не существует).

Из-за этих свойств холодное наблюдение желательно для одних сценариев и нежелательно для других. Например, в приведенном выше примере с интервалом вам обычно нужен холодный подход.

Однако, когда мы рассматриваем наблюдаемые объекты, обертывающие события пользовательского ввода или запросы AJAX, мы будем соответственно вынуждены использовать горячие наблюдаемые объекты или будем очень осторожны, чтобы не вызывать ненужные запросы с холодными наблюдаемыми объектами. Также существует вероятность утечки памяти, когда мы забываем о неиспользуемых, работающих наблюдаемых (особенно холодных).

Чтобы лучше понять горячие наблюдаемые и их свойства, давайте внесем некоторые изменения в наш интервал наблюдаемых, чтобы сделать его горячим.

С новым горячим интервалом, наблюдаемым выше, все подписчики будут получать одни и те же значения независимо от того, когда они подписываются. Однако это еще не все. У горячих наблюдаемых есть и другие свойства:

  • Один существующий производитель обрабатывает все подписки.
  • Производитель генерирует ценности, даже если нет подписчика.

Встроенные функции создания

Знание разницы между холодными и горячими наблюдаемыми становится еще более важным при рассмотрении различных функций создания и других источников наблюдаемых, находящихся вне нашего прямого контроля.

Как правило, наиболее сомнительные наблюдаемые источники должны задокументировать, горячие они или холодные, но если нет, то можно руководствоваться следующим практическим правилом:

Все холодно, если иное не имеет смысла.

Итак, все функции создания, которые мы рассмотрели, fromEvent() холодны. Это имеет смысл, поскольку вы не заставляете события ввода пользователя ждать, пока вы не подпишетесь на наблюдаемое.

Еще одна важная вещь, на которую следует обратить внимание, - это fromFetch() и другие наблюдаемые, связанные с HTTP-запросами, такие как Angular HttpClient. Все они холодные, поэтому, хотя вы можете легко управлять своими запросами с их помощью, вам все равно придется следить за собой, поскольку каждая подписка приводит к другому запросу.

Есть способ сделать холодное наблюдение горячим, и мы немного рассмотрим его, но сначала давайте поговорим о том, как вы можете еще лучше управлять своими подписками.

AsyncPipe

Мы уже рассмотрели, как подписаться на наблюдаемый объект и как управлять подпиской с помощью метода unsubscribe() объекта подписки или, например, complete() внутри функции «blueprint» наблюдаемого объекта.

Однако в Angular есть еще один способ управления подписками, о котором вы должны знать, - это AsyncPipe. Это позволяет вам подписаться на наблюдаемое прямо из шаблона Angular. Более того, он автоматически вызовет unsubscribe(), когда ваш компонент будет уничтожен, что значительно упростит управление подпиской и предотвращение утечки памяти. Он также будет автоматически использовать последнее значение из потока, обновлять представление по мере необходимости и даже повторно подписываться на новый наблюдаемый объект, если это необходимо.

Что касается использования, AsyncPipe имеет простую форму | async, помещенную сразу после наблюдаемого (или обещания) выбора. В следующем примере мы используем его, чтобы получить доступ к данным products$ наблюдаемого, просмотреть их и составить список результатов.

Операторы

Итак, мы довольно хорошо изучили основы наблюдаемых. Мы знаем, как их создавать, подписываться / отказываться от них и вообще контролируем поток уведомлений.

Теперь пора поговорить об операторах - API-интерфейсах управления, благодаря которым «Функциональность» в FRP действительно сияет.

Операторы - это основа библиотеки RxJS. В то время как интерфейс Observable образует прочную основу, волшебство происходит с операторами. Они позволяют вам манипулировать своими коллекциями с помощью составного кода декларативным образом.

Интересный факт: мы уже познакомились с некоторыми операторами. Так называемые «функции создания» на самом деле представляют собой один из двух типов операторов - операторы создания.

Теперь мы откроем второй тип - конвейерные операторы - функции, которые вы можете передать наблюдаемым объектам для создания новых, измененных наблюдаемых объектов с желаемым поведением.

Трубный метод

Есть два способа использовать оператор. Первый - просто вызвать его и передать ему наблюдаемое. Для оператора с именем operator() это выглядело бы так:

Обратите внимание на первые пары скругленных скобок. Он используется для создания / настройки оператора. Если нужны какие-то аргументы, они пойдут сюда. Если нет - вызов остается для согласованности API.

Однако, учитывая составную природу операторов, вы можете увидеть, как приведенный выше синтаксис может быстро стать нечитаемым из-за глубокой вложенности вызовов.

Вот тут-то и пригодится метод pipe() Observable. Он позволяет вам передавать несколько операторов по конвейеру с чистым, читаемым синтаксисом.

Синтаксис pipe() настолько предпочтителен, что рекомендуется использовать его даже для одиночных операторов.

Общие операторы

Теперь, когда мы знаем, как передавать операторы по конвейеру, пришло время изучить некоторые из них. RxJS предоставляет столько операторов, что невозможно охватить их всех в одном сообщении в блоге. Вместо этого мы рассмотрим только некоторые из них, чтобы дать вам пример.

Как и массивы, наблюдаемые объекты и базовые потоки представляют собой последовательности данных - просто асинхронно. Вот почему многие операторы делятся своими примерами использования и именованием с методами массива. Итак, у нас есть filter(), map(), every(), find(), reduce() и многие другие.

Что касается некоторых примеров использования:

Обратите внимание, как операторы импортируются из отдельного модуля - rxjs/operators - вероятно, для организации, поскольку их так много!

Что касается самих операторов - вы можете видеть, что то, как я их использовал, очень похоже на использование методов массива - особенно с числовой наблюдаемой последовательностью.

Однако магия наблюдаемых в том, что они не обязательно должны быть синхронными числовыми данными, с которыми вы имеете дело, и вам не нужно передавать по конвейеру только один оператор за раз. Вы можете смешивать и сопоставлять и выполнять все виды сложных операций асинхронной или синхронизирующей обработки в чистом виде FRP.

Взгляните на следующий пример, где мы напрямую обрабатываем вывод асинхронного запроса выборки:

В fromFetch(), помимо URL-адреса запроса, мы передаем объект конфигурации с обратным вызовом selector() для «разворачивания» извлеченных данных.

Затем, предполагая, что данные JSON представляют собой массив объектов, описывающих пользователей, мы обрабатываем его с помощью нескольких операторов. Сначала concatAll() «разбивает» входной массив на отдельные элементы последовательности, которые затем обрабатываются с помощью filter() и map(). Все чисто и функционально.

Операторы в пользовательском интерфейсе

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

Рассмотрим следующий пример, где мы используем функцию создания fromEvent() и оператор filter(), чтобы определить количество нажатий на кнопку Angular:

Сначала мы используем декораторы ElementRef и @ViewChild, чтобы получить доступ к элементу кнопки DOM. Затем в ловушке ngAfterViewInit(), когда ссылка готова, мы обращаемся к ней и начинаем прослушивать события щелчка с помощью fromEvent(). Пропуская наблюдаемое через filter(), мы проверяем, соответствует ли количество кликов (в свойстве detail) требуемому количеству кликов. Наконец, полученный наблюдаемый объект подписывается и при необходимости генерирует события.

Код функциональный, простой и очень читаемый. Наблюдатели хорошо интегрируются с компонентами Angular и общей структурой.

Что касается примера использования этого компонента:

Преобразование горячего наблюдаемого в холодное

Обсуждая горячие и холодные наблюдаемые, я сказал вам, что есть способ преобразовать холодное наблюдаемое в горячее. Это возможно благодаря некоторым сложным преобразованиям, которые можно использовать с помощью простого оператора share().

Приведенный выше пример - тот же, что мы проанализировали при обсуждении нескольких подписок на одно наблюдаемое. Обратите внимание на небольшую разницу - конвейер оператора share().

Благодаря этому небольшому изменению все наши подписки теперь будут получать одни и те же синхронизированные значения, независимо от того, когда они подписаны. Мы только что сделали горячую наблюдаемую из холодной.

Обработка ошибок

Теперь, зная операторы, мы можем обсудить, как следует обрабатывать ошибки в наблюдаемых объектах.

Мы уже видели это с наблюдателями в subscribe() методе и их обратном вызове error. Однако такой способ обработки ошибок имеет ряд серьезных недостатков. Во-первых, он конечный, то есть вы не сможете легко исправить ошибку, даже если захотите. Кроме того, это противоречит философии Angular разделения ответственности.

Существует специальный оператор, который может использоваться для обработки ошибок и решает обе эти проблемы - catchError(). Чтобы продемонстрировать его использование, давайте вызовем функцию создания fromFetch():

Мы передаем retry() и catchError() нашему fromFetch() наблюдаемому. retry() - еще один полезный оператор, который повторно подпишется на исходный обозреватель в случае ошибки и попытается запустить его до указанного количества повторных подписок.

catchError() получает обратный вызов, который после получения обнаруженной ошибки и наблюдаемого источника решает, выдать ли эту или новую ошибку, вернуть наблюдаемый источник и повторить попытку или вернуть любое другое значение.

В приведенном выше примере я возвращаю Promise.resolve([]) в качестве «резервного значения» в случае ошибки. Это, в свою очередь, приведет к получению пустого массива в конце подписки при возникновении ошибки выборки, поскольку fromFetch() автоматически развернет обещание.

Этот краткий обзор должен дать вам общее представление о том, что такое catchError() и другие операторы обработки ошибок. Они могут показаться здесь немного ненужными, но в более сложных сценариях с вложенными наблюдаемыми объектами, при использовании вместе с AsyncPipe или в других пограничных случаях они становятся бесценными.

Резюме

Этот подробный учебник по RxJS и FRP в Angular должен дать вам твердое представление об основах. Достаточно начать экспериментировать самостоятельно или перейти к изучению более сложных тем, таких как наблюдаемая вложенность, Предметы, Планировщики или сложные операторы.

RxJS и Angular образуют отличную комбинацию, которая при правильном использовании может привести к простому, чистому и приятному коду. Конечно, это требует изучения нескольких новых концепций, но затем эти знания окупаются в виде лучшего кода и лучшего его понимания.