Введение

Я работаю с Angular с версии 1.4, и мне это очень нравится. Когда я переключился на Angular 2, я впервые услышал о «RxJS» и реактивном программировании. Мне немного стыдно признаться, что моей первой реакцией было сказать себе: «Какая разница? Я могу создавать хорошие приложения без него »… И я не ошибся… В самом деле, можно создавать хорошие приложения без« RxJS », точно так же, как можно быть очень хорошим игроком в гольф, используя клюшку для метлы. В конце концов, даже если вы научились очень хорошо играть с клюшкой, усилия по обучению игре с обычной клюшкой помогут вам стать еще лучше.

То же самое с «RxJS» в «Angular». Даже если ничто не заставляет вас использовать его (ну ... Честно говоря, реализация «HttpClient» заставляет вас), вы будете намного эффективнее, а ваше приложение будет более реактивным если вы овладеете им.

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

Вот почему я решил написать серию статей, которые проведут вас через путь изучения того, как эффективно использовать «RxJS» в приложении «Angular». Я не говорю, что то, что вы собираетесь изучить, - это единственный способ использовать RxJS, но это то, как я это делаю, и пока это кажется довольно надежным.

Оглавление

Основные понятия
Субъекты RxJS
Операторы RxJS (Часть 1)
Операторы RxJS (Часть 2)
Операторы RxJS (Часть 3)
TakeUntil и async канал »
Наблюдаемые высшего порядка
Обработка ошибок
Планировщики RxJS (скоро)
Мини-проект: создание Pokedex (вскоре)

В этой статье…

Ладно, поехали ... Тема этой статьи не самая смешная. Действительно, прежде чем начать использовать «RxJS», лучше разобраться в теории, лежащей в основе этого. Я постараюсь не перегружать вас тоннами информации, вместо этого я дам вам самый минимум теории, который вам понадобится в текущей статье, а затем я расскажу о том, что я остановил, в следующих статьях. Так что выпрямите маленького солдата и будьте храбрыми, как только вы прочитаете эту статью, вы будете готовы немного повеселиться с большим количеством программирования :-)

Реактивное программирование

«Rx» в «RxJS» на самом деле означает «ReactiveX», который представляет собой набор инструментов, которые вы можете использовать для реактивного программирования. Действительно, вы должны знать, что реактивное программирование не является специфическим для «JavaScript». Вы можете выполнять реактивное программирование на «Java», используя библиотеку «RxJava», или на C #, используя пакет «System.Reactive». Это означает, что «RxJS» - это «просто» библиотека для использования в «JavaScript» для реактивного программирования.

Если ваша жизнь разработчика похожа на меня, вы, вероятно, в основном привыкли к другой парадигме программирования, такой как программирование, управляемое событиями, ориентированное объектное программирование и тому подобное, что, вероятно, является основной причиной того, почему изучение реактивного программирования так сложно. Это совершенно другая парадигма, и может быть трудно изменить свое мышление, чтобы принять ее, но не бойтесь, мы справимся с этим :-)

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

Допустим, вы создаете приложение для чата. Для этого вы, вероятно, захотите использовать соединение через веб-сокет, позволяющее вашему «Angular» приложению постоянно подключаться к вашему серверу. Таким образом, когда кто-то что-то вам сообщает, сервер может отправлять данные в ваше «Angular» приложение, даже не запрашивая его. В этом случае ваше приложение «Angular» получит новые данные, которые обновят данные вашего приложения, и, следовательно, приложение должно будет реагировать на эти новые данные. Здесь в игру вступает реактивное программирование. Это позволит вашему приложению «прислушиваться к этим изменениям» и реагировать на них, например, обновляя основной компонент, чтобы отображать полученные сообщения вместе с другой информацией (кто их отправил, когда и т. Д.…).

Наблюдаемые и наблюдатели

Теория

Реактивное программирование построено на двух важных концепциях: наблюдаемые и наблюдатели.

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

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

Веселая часть

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

Хорошо, так что у нас здесь?

Наблюдаемое

Создать наблюдаемый объект так же просто, как вызвать конструктор Observable, как показано в строке 3. Этот конструктор ожидает функцию с одним параметром, который является наблюдателем. Здесь важно отметить, что мы не создавали наблюдателя, а только наблюдаемое. Как мы уже говорили ранее, наблюдатель является потребителем данных, которые излучает наблюдаемый объект, поэтому создание наблюдателя остается на усмотрение кода, заинтересованного излучаемыми значениями.

Переменная с именем «наблюдатель» в приведенном выше коде на самом деле является ссылкой на наблюдателя (ов), подписанного на наблюдаемый объект. Но тогда… Как нам создать этих наблюдателей? Что ж, я объясню это через несколько секунд, но давайте сначала завершим объяснение наблюдаемого.

Мы называем «подпиской» факт подключения наблюдателя к наблюдаемому, поэтому наблюдатель получит данные наблюдаемого только после того, как подписался на него.

Здесь для каждого наблюдателя, подписанного на наблюдаемое, мы начинаем с создания локальной переменной с именем «x», затем мы запускаем интервал, который будет вызывать «Observer.next (++ x)» каждую секунду.

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

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

Знак «$» в конце имени переменной - это соглашение об именовании, которое явно указывает на то, что мы имеем дело с наблюдаемой.

Наблюдатель

Теперь, когда у нас есть наблюдаемое, пора подписаться на него вместе с наблюдателем. Есть разные способы сделать это, но давайте начнем с более разумного:

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

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

У наблюдаемого может быть столько наблюдателей, сколько мы хотим. Например, следующий код отлично подходит:

Это производит:

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

Я использовал console.warn вместо console.log просто для большей наглядности этих журналов:

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

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

Подписка

Подписка на наблюдаемое, конечно, очень важно, но и отказ от подписки не менее важен. Действительно, когда вы подписываетесь на наблюдаемый объект, созданный в предыдущем фрагменте, вы запускаете создание цикла интервала, который выдает значение каждую секунду. А теперь давайте представим, что у вас есть кнопка, которую вы можете нажать, чтобы прекратить прослушивание генерируемых значений, как бы вы это сделали?

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

Поскольку мы хотим убить подписку при нажатии кнопки, мы сохраняем ее в частной переменной при подписке на наблюдаемое. При нажатии на кнопку вызывается функция «убить». Попробуем сделать это через 2–3 секунды:

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

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

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

И вот мы, если вы попробуете то же самое, что и раньше, вы увидите, что нажатие на кнопку теперь останавливает отображение всех журналов в консоли.

Итак, теперь вопрос, который нужно задать себе: «Когда мне следует отказаться от подписки на наблюдаемое?». И ответ ... дан далее в статье. В самом деле, чтобы дать четкий ответ на этот вопрос, нам нужно увидеть, как обрабатывать ошибки и завершать наблюдаемое.

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

В идеальном мире нам не нужно было бы обрабатывать ошибки, все было бы максимально гладко, без ошибок. Однако, если вы программируете более одного дня, вы, вероятно, знаете, что ошибки неизбежны и должны обрабатываться правильно, поэтому возникает вопрос: «Как я могу обрабатывать ошибки в наблюдаемом?».

Что ж, ответ довольно прост:

Ошибка может быть выброшена из наблюдаемого, вызвав функцию «error» для наблюдателя. Если вы сделаете только это, ошибка не будет обнаружена и закончится в консоли:

Однако есть простой способ отловить ошибку. Если вы понимаете, как наблюдатели связаны со своими наблюдаемыми объектами, вы, вероятно, не удивитесь, увидев следующий код:

Поймать наблюдаемую ошибку так же просто, как предоставить функцию обратного вызова «error» при подписке на наблюдаемую. Если вы это сделаете, эта функция будет выполняться при возникновении ошибки с использованием функции «error» наблюдателя.

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

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

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

Завершите наблюдаемое

Наблюдаемое не обязательно должно излучать значения ad vitam eternam. В самом деле, давайте представим, что вы хотите создать наблюдаемое, которое имитирует обратный отсчет от 5 до 0. Хорошо, этот наблюдаемый должен выдавать 5 значений (4, 3, 2, 1 и 0), затем прекратить это делать, и наблюдатели должны знать, когда наблюдаемое имеет завершенный. Вы можете добиться этого следующим образом:

  1. Вызов «полной» функции наблюдателя
  2. Обеспечение «полного» обратного вызова при подписке

Здесь мы немного обновили код, чтобы уменьшить значение «x» с 5 до 0 и завершить наблюдаемое, когда оно достигнет 0. Обратите внимание, что еще раз мы удаляем интервал при завершении наблюдаемого, чтобы гарантировать, что все ресурсы, выделенные нашим наблюдаемые высвобождаются. Чтобы поймать завершение наблюдаемого, мы просто определили «полный» обратный вызов при подписке на наблюдаемое. Обратите внимание, что этому обратному вызову не передаются никакие данные, так как все значения наблюдаемого должны быть переданы через функцию «next». «Полный» используется только для того, чтобы указать наблюдателям, что наблюдаемое завершено и не будет выдавать новых значений.

Когда отказаться от подписки?

Вернемся к вопросу «когда мне отказаться от подписки?». Ответ немного зависит от контекста. Если вы подписываетесь на наблюдаемый объект, который завершается, может не потребоваться отписываться от него. Действительно, когда наблюдаемый завершается (или выдает ошибку), связь между наблюдаемым и наблюдателем разрывается, поэтому нет необходимости отказываться от подписки.

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

Однако, если вы подписались на «бесконечную наблюдаемую», очень важно отписаться от нее. Типичный «Angular» случай - это подписаться на наблюдаемый объект в хуке «ngOnInit», сохранить подписку в частной переменной и отказаться от подписки в хуке «ngOnDestroy», чтобы гарантировать, что подписка будет уничтожена при уничтожении компонента.

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

Другой синтаксис

Способ подписаться на наблюдаемое, которое мы только что видели, не единственный. Вы также можете использовать синтаксис, в котором вы передаете «следующий» обратный вызов в качестве первого параметра «подписки», обратный вызов «ошибка» в качестве второго параметра и «завершенный» обратный вызов в качестве третьего параметра. Итак, следующий код:

Можно было написать:

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

Горячие наблюдаемые против холодных наблюдаемых

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

  • Наблюдаемый объект считается холодным, когда производитель данных создается, когда наблюдатель подписывается на него.
  • А наблюдаемый считается горячим, когда производитель данных создается независимо от подписки наблюдателей.

Не очень понятно, а? Что ж, начнем с определения производителя. Производитель - это просто объект, который производит данные, которые генерируются наблюдаемым объектом. В предыдущих примерах производителем была функция «setInterval».

Обладая этой информацией, вы, вероятно, сможете ответить на вопрос «Были ли наблюдаемые в примере горячими или холодными?».

Если бы вы хорошо поняли все мои объяснения, вы бы догадались, что ответ в том, что они холодные. Действительно, производитель («setInterval») вызывается в конструкторе наблюдаемого, что означает, что он вызывается каждый раз, когда новый наблюдатель подписывается на наблюдаемый, поэтому, если у нас есть два наблюдателя, подключенных к наблюдаемому, у нас фактически есть два интервала. и испускаемые значения не разделяются между разными наблюдателями.

Это можно проверить с помощью следующего кода:

В этом примере производителем значений по-прежнему является функция «setInterval», поэтому мы знаем, что наблюдаемое является холодным. Если мы исследуем испускаемые значения:

Здесь мы можем заметить, что через одну секунду значение «11» выдается первой подпиской, а значение «7» - второй. Это иллюстрирует тот факт, что даже несмотря на то, что мы подписались на одну и ту же наблюдаемую, выдаваемые значения независимы для каждого наблюдателя.

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

Последние мысли

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

Следующая статья посвящена темам« RxJS и управлению подписками».