Около 4 месяцев назад я решил провести небольшой эксперимент. Планировалось создать визуализатор алгоритмов сортировки, но суть заключалась не в самих алгоритмах. Моей целью было:
- Посмотрите, насколько хорошо RxJS работает с React: я рассматривал redux-observable как промежуточное ПО для обработки асинхронности и побочных эффектов. Прежде чем опробовать библиотеку, я хотел увидеть, как я сам подхожу к использованию RxJS с React.
- Посмотрите, насколько «легко обосновать» код RxJS: у меня создалось впечатление, что потоковые библиотеки (и функциональное программирование в целом) поначалу довольно сложно изучить, но становятся интуитивно понятными, как только они усваиваются. .
Итак, план заключался в том, чтобы написать работающее приложение, а затем не трогать его в течение длительного периода времени, а потом вернуться к нему и провести рефакторинг. К моему удовольствию, работать с RxJS было безболезненно, и его также легко было реорганизовать через 4 месяца. Этот пост будет кратким обзором моих личных выводов.
Redux застрял в моей голове
Даже после того, как я намеренно исключил Redux из своего проекта, я, естественно, попытался имитировать его. Я использовал веб-API CustomEvent (удачно разделенный именами как «действие») для отправки событий,
this.goToNextStep = () => { const action = { origin: ‘USER’, request: ‘GO_TO_NEXT_STEP’ } document.dispatchEvent(new CustomEvent(‘action’, {detail: action})) }
и создать наблюдаемое событие «действия» в компоненте React <StreamProvider />
с подпиской, которая будет запускать обновления состояния, которые передаются дочернему компоненту <SortVisualizer />
.
this.sortHistory$ = Observable .fromEvent(document, 'action').map(e => e.detail) .mergeMap(...) // deal with actions this.sortHistory$ .subscribe(x => { this.setState({ sortState: x[x.length - 1] }) })
Стримы классные
Отчасти проблема заключалась в том, чтобы не использовать никаких комментариев. Может ли мой код, основанный только на хороших методах именования и организации, быть легким для чтения?
Трудно что-либо сказать о высокоуровневой организации кода приложения, потому что даже после 4+ месяцев бездействия было легко вспомнить, что и почему мои дизайнерские решения после нескольких минут копания.
Однако было действительно здорово испытать, насколько легко было следовать частям, написанным на RxJS.
Реализовать «Отменить» легко с RxJS
Создание кнопки «вернуться на один шаг» с использованием RxJS было легким делом. Во-первых, я отслеживал текущее состояние процесса сортировки как опору in<SortVisualizer />
следующим образом:
const currentSortState = { bars: [{bar}, ...] // data that we're sorting nextStep: {targetIndex: 0, type: 'COMPARE'} // describes what the next sorting step is }
Это свойство взято из наблюдаемого в <StreamVisualizer />
. Метод .scan()
RxJS значительно упростил задачу:
this.sortHistory$ = this.actions$ // other stuff .scan((acc, curr) => { if (curr.request === 'GO_TO_PREV_STEP') { // if want to go back return acc.length <= 1 ? acc : acc.slice(0, acc.length -1) // just remove latest step } else { // other stuff } })
Декларативная обработка асинхронного режима с помощью RxJS
Еще один аспект, на котором я сосредоточился, заключался в том, насколько легко обрабатывать асинхронные операции с помощью RxJS. Опять же, мне это понравилось. Потоки позволили мне писать асинхронный код более декларативным образом.
Например, я хотел иметь функцию «автовоспроизведение», которая будет отключаться всякий раз, когда пользователь пытается сделать что-то еще, например перетащить полосу, чтобы не отпустить ее. Вместо того, чтобы императивно говорить: «Если произойдет событие А, выключите автовоспроизведение. Если произойдет событие B, выключите автовоспроизведение. Если событие C… »Я мог легко сказать« автоматическое воспроизведение, пока не произойдет какое-либо другое событие ».
const handleAutoPlay = (action) => { if (!this.state.playing && action.request === 'TOGGLE_PLAY') { // to turn on autoplay return Observable.interval(500) // every 500ms .startWith(1) .takeUntil(this.actions$) // until we get some other action .takeWhile(x => !this.state.sortState.sortCompleted) // or until sorting is completed .map(x => {return {request: 'GO_TO_NEXT_STEP'}}) // fire 'GO_TO_NEXT_STEP' .do(x => { if (!this.state.playing) {this.setState({playing: true})} }) .finally(() => {this.setState({playing: false})}) } else { return Observable.of(action) } } this.sortHistory$ = this.actions$ .mergeMap(handleAutoPlay) // other stuff
В заключение
Кодовая база определенно может потребовать больше работы (например, события отправки разбросаны по <StreamProvider />
и <SortVisualizer />
, yuk), но на этом я пока завершу свой эксперимент.
В целом экспериментом я доволен. Это потому, что это вселило в меня уверенность в том, что я должен продолжать учить себя функциональному программированию. Это может быть болезненно (чтение о монадах вредит моему мозгу), но я уверен, что это окупится.
Спасибо за чтение.