Ваш лучший друг при работе с несколькими наблюдаемыми потоками.

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

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

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

Что такое операторы преобразования?

Операторы преобразования, как следует из названия, позволяют преобразовывать значения по мере их прохождения через наблюдаемый поток. В моей разработке операторы преобразования были ключевыми при работе с запросами API и обработкой входящих данных. Вы обнаружите, что эти операторы экономят день, поэтому вам не нужно иметь дело с несколькими вложенными подписками. Наиболее распространенные операторы преобразования относятся к определенному подразделу, известному как операторы выравнивания, например: mergeMap, concatMap, switchMap и exhaustMap.

Что такое сглаживающий оператор?

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

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

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => event.target.value)
).subscribe(console.log)

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

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
     response.subscribe(console.log)
});

Хотя это работает, когда ответ успешно регистрируется в консоли, это действительно не идеально по нескольким причинам.

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

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

input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
      response.pipe(
         map(({login, id}) => { 
          return of(`Do not do this. This is bad.`);
         })
      ).subscribe(response2 => { 
                  response2.subscribe(console.log)
                 })
});

Вы можете видеть, как это начинает становиться головной болью.

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

input$.pipe(
   map(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
   mergeAll()
).subscribe(console.log);

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

Понимание оператора mergeMap

Наиболее распространенным оператором выравнивания (на мой взгляд) должен быть оператор mergeMap. Давайте возьмем пример выше и очистим его еще больше. Итак, вместо использования map, затем mergeAll. Мы объединяем их и используем mergeMap:

input$.pipe(
   mergeMap(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
).subscribe(console.log);

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

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

Понимание оператора switchMap

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

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

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

Чтобы понять, чем switchMap отличается от mergeMap, давайте начнем с нового примера. В этом примере у нас есть поток событий щелчка, который мы сопоставляем с внутренним интервалом при каждом щелчке.

const interval$ = interval(1000);
const click$ = fromEvent(document, 'click');
click$.pipe(
    mergeMap(() => interval$)
).subscribe(console.log)

При первом щелчке создается интервал. При втором щелчке создается второй интервал, в то время как первый интервал остается. Если есть еще один щелчок; создается третий интервал. Консоль будет выглядеть примерно так:

0 1 2 3 0 4 1 5 2 6 0 3 7 1

Теперь давайте заменим mergeMap на switchMap здесь.

Вы увидите, что поведение будет отличаться.

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

click$.pipe(
    switchMap(() => interval$)
).subscribe(console.log)
// outcome: 0 1 2 3 0 1 2 0 1 2 3

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

Теперь, когда мы лучше понимаем поведение оператора switchMap, давайте рассмотрим реальный пример:

const searchInput = document.getElementByID('search-box');
const input$ = fromEvent(searchInput, 'keyup')
input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged()
).subscribe(console.log)

Как и в нашем первом примере, у нас есть еще один поток событий «keyup» из ввода поиска.

Сначала мы устраняем дребезг потока с помощью оператора debounceTime, чтобы выдавать последнее значение только после паузы в 200 мс. Затем мы используем оператор pluck, чтобы выбрать свойство вложения целевого значения, а затем применяем оператор distinctUntilChanged(), чтобы гарантировать, что выдаются только уникальные значения. Таким образом, когда пользователь вводит поисковый ввод, после того, как пользователь делает паузу в течение 200 мс или более, значение выдается на консоль.

Давайте добавим switchMap, чтобы завершить эту функциональность:

input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged(), 
  switchMap(searchTerm => { 
     return ajax.getJSON(`http://api.som.org/${searchTerm}`)
  })
).subscribe(console.log)

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

Я слышал, вы спрашиваете, зачем использовать switchMap вместо mergeMap для этой функциональности?

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

С оператором switchMap он переключается на новую наблюдаемую, отменяя предыдущую внутреннюю наблюдаемую, включая запрос, если испускается обновленное значение.

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

Ссылки: некоторые примеры кода были взяты из курса RxJs Basic от Ultimate Courses.