Недавно моя работа потребовала от меня ознакомиться с новой инфраструктурой Angular 2 и некоторыми связанными с ней инструментами, такими как @ngrx/store. Наряду с этим появляется концепция реактивного программирования. Это было не совсем интуитивно понятно мне или моим коллегам, но поскольку я чувствую, что теперь у меня есть контроль над этим, этот пост предназначен для их пользы и для пользы всех, кто не понимает, что такое Observable, Observer, Subject, подписка, и т.д.

Итак… Что такое реактивное программирование? Это не большой грузовик, это серия труб!

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

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

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

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

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

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

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

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

Теперь одно действие по вводу нового имени вызовет двух потребителей! Аккуратный!

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

Этот «молния» — это специальный компонент, в который мы можем передавать каналы, и он будет брать значения из каждого входящего канала, группируя значения один к одному. Например, предположим, что получение инициала «Джон» — это очень трудоемкая задача. «Zip» получает «Smith» из канала с последним именем, но не передает его потребителю до точки, где «J» происходит от первой буквы устройства. Это так, даже если появляются другие имена — они просто выстраиваются в очередь. Как только появляется «J», Zip объединяет его с ожидающим «Smith» и отправляет потребителю.

Говоря в манере процессинга, это крайне полезное поведение! Это означает, что процессор первой буквы асинхронен — он не заставляет всех остальных ждать, пока он выполнит свою работу. Единственные вещи, которые ждут от него, это те, которые действительно требуют его вывода (например, Zip).

Куда это вообще идет?!

Во всем этом есть смысл, клянусь! Вернемся в мир реального кода.

Angular 2 и его экосистема основаны на инструментарии ReactiveX, а именно на rxjs. Они используют другую, более техническую номенклатуру, чем я использовал выше, но многие из понятий одинаковы. Давайте посмотрим на некоторый код для выполнения нашего первого конвейера: печать первого имени входного полного имени.

let inputPipe$ = Rx.Observable.of('John Roger Smith');
let splitData$ = inputPipe.map(fullName => fullName.split(' '));
let firstName$ = splitData.map(splitName => splitName[0]);
firstName$.subscribe(name => console.log(name));

Это выглядит немного ужасно, но потерпите меня. В Rxjs есть концепция, называемая Observable, которая служит той же цели, что и наши трубы/пайпы в приведенном выше примере. Каждая из переменных в приведенном выше коде содержит один такой Observable. inputPipe$ инициализируется как Observable, который будет передавать через себя одно значение: «Джон Роджер Смит». Знак доллара в конце — это просто соглашение об именах, позволяющее различать наблюдаемое значение и само статическое значение.

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

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

Код, вероятно, выглядит немного громоздким. Это связано с тем, что rxjs предназначен для «функционального» использования, связывая вызовы в цепочку, а не сохраняя их результаты в переменных, когда это возможно. Давайте сделаем это, но на самом деле реализуем всю обработку, чтобы напечатать «J Smith».

let source$ = Rx.Observable.of('John Roger Smith');
let splitName$ = source$
  .map(name => name.split(' '));
let firstInitial$ = splitName$
  .map(splitName => splitName[0])
  .map(firstName => firstName[0]);
let lastName$ = splitName$
  .map(splitName => splitName[splitName.length - 1]);
let initialedName$ = Rx.Observable.zip(firstInitial$, lastName$)
  .map(zipArray => zipArray[0] + ' ' + zipArray[1]);
initialedName$.subscribe(name => console.log(name));

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

Чтобы продемонстрировать это, давайте воспользуемся еще одним инструментом из набора приемов rxjs: темами. Субъекты — это объекты, которые действуют как Observables, но могут получать значения и действовать как источники.

let source$ = new Rx.Subject();
// Rest of code from above
source$.next('John Roger Smith');
source$.next('Sarah A Q Flanders');
source$.next('Charlie Vanderbilt');

Это должно распечатать «J Smith», «S Flanders» и «C Vanderbilt».

Эта способность определять потоки данных делает реактивное программирование превосходным для одностраничных приложений или других долгоживущих процессов в браузере и вне его. Он позволяет объявлять изменяющиеся значения абстрактным образом, избегая путаницы определений функций и избегая ада обратных вызовов. Черт возьми, в Angular 2 отобразить initialedName$ так же просто, как включить это в свой HTML-шаблон:

{{ initialedName$ | async }}

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