Давайте начнем с понимания того, что такое «реактивное программирование»:
Реактивное программирование - это асинхронная парадигма программирования, связанная с потоками данных и распространением изменений.
- Википедия
ReactiveX или Rx - самый популярный API для реактивного программирования. Он построен на идеологиях наблюдаемого шаблона, шаблона итератора и функционального программирования. В Rx есть библиотеки для разных языков, но мы будем использовать RxJS.
Rx основан на Observables, Observers и Operators.
По сути, Observer подписывается на Observable.
Затем Observable испускает потоки данных, которые Observer прослушивает и реагирует, запуская цепочку операций с потоком данных. Реальная мощность исходит от операторов или «реактивных расширений» (отсюда и термин Rx) .
Операторы позволяют вам преобразовывать, комбинировать, манипулировать и работать с последовательностями элементов, генерируемых Observables.
Если вы не знакомы с Rx, вам может быть трудно понять и использовать Redux-Observable. Так что я предлагаю вам сначала испачкать руки Rx!
Теперь перейдем к использованию RxJS с Redux.
Redux-Observable
Redux-Observable - это промежуточное ПО на основе RxJS для Redux
Вот что Redux Docs говорит о промежуточном программном обеспечении в Redux:
Промежуточное ПО обеспечивает стороннюю точку расширения между отправкой действия и моментом его достижения редуктором.
Промежуточное ПО Redux можно использовать для ведения журнала, отчетов о сбоях, взаимодействия с асинхронным API, маршрутизации и многого другого. Или мы можем сказать побочные эффекты в целом.
Так как же Redux-Observable все это делает?
Через Эпосы. Эпики - это основной примитив Redux-Observable. Эпопея - это простая функция, которая выполняет действие, а затем возвращает другое действие. Действие в → Действие нет. Таким образом, действия рассматриваются как потоки.
Каждое действие, отправленное в любом компоненте React, будет проходить через такие функции (эпики) как поток.
Давайте посмотрим, как выглядит простой эпос, который принимает action
'PING’
и возвращает новое action
'PONG’
:
const pingEpic = action$ =>
action$.filter(action => action.type === 'PING')
.mapTo({ type: 'PONG' })
$
после action
используется, чтобы указать, что эти переменные ссылаются на потоки. Итак, у нас есть поток действий, передаваемый в Epic, в котором мы использовали оператор filter
RxJS.
Этот оператор фильтра отфильтровывает все действия, не относящиеся к type
PING
! Следовательно, Epic pingEpic
занимается только обработкой действий type
‘PING’
. Наконец, этот action
‘PING’
отображается на новый action
из type
‘PONG’
, удовлетворяющий основному правилу Эпиков: Действие в → Действие завершено.
Поскольку каждый эпос касается только определенного типа действий, у нас есть специальный оператор для action$
(поток), чтобы отфильтровать нежелательные действия из потока. Этот оператор является оператором ofType()
.
Переписывая предыдущий эпос с использованием ofType
, получаем:
const pingEpic = action$ => action$.ofType('PING') .mapTo({ type: 'PONG' })
Если вы хотите, чтобы ваш эпос допускал более одного типа действий, оператор ofType()
может принимать любое количество аргументов, например: ofType(type1, type2, type3,...)
.
Знакомство с особенностями работы эпиков
Вы можете подумать, что действие «PING» просто приходит и поглощается этой эпопеей. Это не относится к делу. Всегда нужно помнить две вещи:
- Каждое действие всегда сначала выполняется редуктором
- Только после этого действие получило эпос
Следовательно, цикл Redux работает нормально, как и должен.
action
‘PING’
сначала достигает редуктора, а затем его получает Epic, затем изменяется на новый action
‘PONG’
, который отправляется редуктору.
Мы даже можем получить доступ к состоянию магазина внутри Epic, потому что второй аргумент Epic - это облегченная версия Redux Store! См. Ниже: const myEpic = (action$, store) =>
Мы можем просто вызвать store.getState()
и получить доступ к состоянию внутри Epics.
Сцепление операторов
Между получением действия и отправкой нового мы можем выполнять всевозможные асинхронные побочные эффекты, которые захотим, такие как вызовы AJAX, веб-сокеты, таймеры и т. Д. Это делается с помощью многочисленных операторов, предоставляемых Rx.
Эти операторы Rx позволяют объединять асинхронные последовательности декларативным образом со всеми преимуществами эффективности обратных вызовов, но без недостатков вложенных обработчиков обратных вызовов, которые обычно связаны с асинхронными системами.
Мы получаем преимущества обратных вызовов без этого пресловутого «ада обратных вызовов».
Посмотрите, как мы можем использовать возможности операторов ниже.
Типичный вариант использования
Предположим, что мы хотим найти слово с помощью чего-то вроде словарного API, используя текст, введенный пользователем в режиме реального времени. В основном мы занимаемся хранением (в магазине Redux) и отображением результатов вызова API. Мы также хотели бы отклонить вызов API, чтобы API вызывался, скажем, в течение 1 секунды после того, как пользователь прекращает печатать.
Вот как это будет сделано с помощью операторов Epic и RxJS:
const search = (action$, store) => action$.ofType('SEARCH') .debounceTime(1000) .mergeMap(action => ajax.getJSON(`https://someapi/words/${action.payload}`) .map(payload => ({ type: 'SET_RESULTS', payload })) .catch(payload => Observable.of({type: 'API_ERROR', payload})) )
Слишком много, чтобы справиться?! Не волнуйтесь, давайте разберемся с этим.
Эпопея получает поток действий все oftype
‘SEARCH’
. Поскольку пользователь постоянно набирает текст, полезная нагрузка каждого входящего действия (action.payload
) содержит обновленную строку поиска.
Оператор debounceTime()
используется для фильтрации некоторых действий в потоке, кроме последнего. Он в основном передает действие через него, только если истекла 1 секунда, но он не получил другого действия или наблюдаемого.
Затем мы выполняем запрос AJAX, сопоставляя результаты с другим действием 'set_RESULTS'
, которое принимает данные ответа (payload)
редуктору, который является частью Action Out.
Любые ошибки API выявляются с помощью оператора catch
. Создается новое действие с подробными сведениями об ошибке, а затем отображается тостер с сообщением об ошибке.
Обратите внимание, как ловушка находится внутри mergeMap()
и после запроса AJAX? Это потому, что mergeMap()
создает изолированную цепочку. В противном случае ошибка достигнет ofType()
и остановит наш Epic. Если это произойдет, Epic перестанет прислушиваться к любым действиям в будущем!
Мы также можем использовать традиционные обещания для запросов AJAX. Однако у них есть неотъемлемая проблема невозможности быть отмененной. Итак, еще один важный вариант использования Epics - это отмена AJAX.
Мы используем оператор takeUntil
для решения этой проблемы. Это делается так же, как мы использовали этот оператор catch
внутри mergeMap
и после запроса AJAX.
Это потому, что takeUntil
должен останавливать текущий запрос AJAX, а не весь Epic! Следовательно, здесь также важна изоляция цепочек операторов.
Ослабление, регулирование, фильтрация, отмена AJAX и другие - это лишь верхушка айсберга. В нашем распоряжении множество операторов, что упрощает решение сложных сценариев использования. Используя эти операторы, вы можете проявить столько творчества, насколько позволяет ваше воображение! Функциональное реактивное программирование (FRP) по-своему элегантно.
В этой статье я сосредоточился на части объяснения FRP в Redux с использованием Redux-Observable. Для настройки Redux-Observable в React + Redux обратитесь к официальной документации - она очень хорошо документирована, подробна и проста.
Обязательно ознакомьтесь с другой моей статьей о Redux, в которой исследуются лучшие практики создания редукторов:
Хотите улучшить свои основы JavaScript? Прочтите это:
Мир ✌️