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

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

  • 14 марта
  • читать 20 мин
Владимир Шайтан
Владимир Шайтан Senior Full Stack Developer в UKEESS Software House, Преподаватель Компьютерной школы Hillel.
Дмитрий Солтановский
Дмитрий Солтановский Junior Frontend-developer | JavaScript | React

Было такое, что вы находили гениальное, непревзойдённое решение проблемы в коде, всё работает идеально, но коллега спрашивает: «А что, если изменится этот параметр?». И сразу оказывается, что даже самая простая, на первый взгляд, конструкция оборачивается клубком взаимосвязанных задач. Знакомо? Именно здесь начинает сиять реактивное программирование.

ЧТО ТАКОЕ РЕАКТИВНОЕ ПРОГРАММИРОВАНИЕ?

Если вы иногда встречались с асинхронным JavaScript, но пока не углублялись в детали, советую сначала заглянуть в документацию на MDN ("Introducing async JavaScript"). Если с этим всё хорошо, тогда перейдём к существу.

Реактивное программирование — это парадигма, построенная вокруг потоков данных и автоматического распространения изменений.

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

ОСНОВНАЯ ИДЕЯ

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

КАКИЕ ПРЕИМУЩЕСТВА РЕАКТИВНОГО ПРОГРАММИРОВАНИЯ?

Давайте посмотрим на преимущества. Конечно, вспомним об асинхронности, потому что это едва ли не ключевая часть.

  1. Простота обработки асинхронности. Никто не любит сотни колбеков, промесей и возможных «утечек». Реактивный подход позволяет легко настроить поток, который будет автоматически реагировать на поступления новых данных. Код сокращается, а нервы — целые.
  2. Меньше кода для управления состоянием. Нет необходимости вручную обновлять переменные или сохранять ссылки, чтобы отслеживать любой клик. Если в одном месте что-то изменилось — реактивная система сама «подхватит» это событие. Это экономит часы кода и спасает от дублирования.
  3. Масштабируемость. Когда ваш маленький проект превращается в большой веб-приложение, количество зависимостей резко возрастает. В реактивной системе этот рост не пугает: чем больше у вас изменений, тем органичнее система «находит» куда их ретранслировать. Производительность и понятность остаются на высоком уровне даже в масштабных решениях.

ГЛАВНЫЕ ЭЛЕМЕНТЫ РЕАКТИВНОГО ПРОГРАММИРОВАНИЯ

Чтобы поближе понять, как это работает, стоит узнать о базовых понятиях.

  1. Потоки данных (Streams). Представьте их как «трубопроводы», по которым непрерывно протекает информация. Вы можете подключиться к этому потоку, а система позаботится о том, чтобы вы всегда получали актуальные данные.
  2. Наблюдатели (Observers). Они слушают потоки и реагируют, когда появляется новая порция информации. Это как зарплата, которая «прилетает» на карту в неожиданный момент: достаточно быть подписано на обновление, чтобы сразу это заметить.
  3. Операторы (Operators). Это небольшие «рабочие», превращающие данные по определённым правилам. Например, filter пропустит только те элементы, которые соответствуют определённым условиям, а map может перевести число в его квадрат или объект в другую структуру.

Пример

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

// Поток, в котором хранится текущий город

const selectedCity$ = new BehaviorSubject('Kyiv');

// Если город меняется, автоматически отправляем запрос и получаем результаты

selectedCity$
  .pipe(
    switchMap(city => fetchFlightsForCity(city))
  )
  .subscribe(flights => updateUI(flights));

Вы указали только исходную точку (selectedCity$) и функцию, вызывающую API. Если пользователь выбирает другой город, предыдущий запрос автоматически отменяется, а новый немедленно стартует. Невероятно удобно.

КАК ЭТО РАБОТАЕТ ГЛУБЖЕ?

Все крутится вокруг того, что приложение воспринимает события (изменения) как «сигналы». Как только сигнал появляется, он путешествует по потоку и изменяет конечные точки. Вам не нужно писать десяток условий в разных местах — всё централизовано.

Пример: обновление UI при смене города

Пользователь выбирает другой город.selectedCity$ шлёт сигнал: «Город изменен». Система дает команду: «Отмени старый запрос» и «Выполни новый». Когда данные поступают в поток, subscribe подхватывает результат и вызывает updateUI, обновляя интерфейс. То есть никакого ручного "if cityChanged" — всё на плаву.

ОБРАБОТКА ОШИБОК В РЕАКТИВНОМ ПОДХОДЕ

На самом деле даже ошибки — это тоже событие, которое может передаваться в потоке. То есть когда случилась ошибка, вы можете решить, что делать:

  • вернуть кэшированные данные;
  • показать сообщение пользователю;
  • повторить запрос несколько раз.

Примеры

// Если сервер не отвечает, возвращаем «резервные» данные

const fetchData$ = throwError(() => new Error('Сервер недоступний')).pipe(
  catchError(() => of('Дані з кешу'))
);

// Несколько повторных попыток при проблемах с сетью

const unstableRequest$ = throwError(() => new Error('Проблема з мережею')).pipe(
  retry(3),
  catchError(error => of(`Запит провалено: ${error.message}`))
);

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

ОСНОВНЫЕ ИНСТРУМЕНТЫ

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

  1. RxJS. Самая популярная библиотека для JavaScript, предоставляющая средства управления потоками (Observable, Subject и многие операторы). Безумно мощная, широко используется во фронтенде.
  2. React + Redux-Observable. Если вам нравится React и Redux, то Redux-Observable позволит обрабатывать сложные асинхронные процессы в виде эпиков (epics). Легко масштабировать, легко тестировать.
  3. Vue.js. Даже если Vue не зовёт себя «реактивным фреймворком», под капотом он реализует реактивную модель: изменили переменную — и DOM автоматически отразил эти изменения.

RxJS пример с полем поиска

import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

const searchInput = document.getElementById('search');

fromEvent(searchInput, 'input')
  .pipe(
    debounceTime(300),
    map(event => event.target.value)
  )
  .subscribe(value => console.log(value));

Вместо множества таймеров и ручной отмены запроса при каждом нажатии debounceTime(300) задерживает вызов на 300 мс, и вы получаете спокойный поток «актуального» текста.

React + Redux-Observable

Обычный React не слишком «реактивный» с точки зрения управления потоками, но с Redux-Observable ситуация меняется.

import { ofType } from 'redux-observable';
import { switchMap, catchError, map } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { of } from 'rxjs';

const fetchUserEpic = (action$) => 
  action$.pipe(
    ofType('FETCH_USER'),
    switchMap(() =>
      ajax.getJSON('https://jsonplaceholder.typicode.com/users/1').pipe(
        map((response) => ({
          type: 'FETCH_USER_SUCCESS',
          payload: response,
        })),
        catchError((error) =>
          of({
            type: 'FETCH_USER_ERROR',
            payload: error.message,
          })
        )
      )
    )
  );

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

Vue.js

У Vue все работает «из коробки». Если вы объявляете переменную в data(), Vue автоматически отслеживает ее изменения и перерендерирует интерфейс. Например:

<template>
  <div>
    <p>Значення лічильника: {{ count }}</p>
    <button @click="increment">Додати</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};
</script>

При каждом клике count увеличивается, а Vue автоматически обновляет DOM. Это и есть один из самых простых примеров «реактивности».

РЕАКТИВНОЕ ПРОГРАММИРОВАНИЕ ДЛЯ ВСЕХ?

Может показаться, что как только вы увидели «реактивный» подход, то быстро перейдёте на него во всём. И не всё так просто.

  1. Кривая уобучения. Сначала сложно понять, как «потоки» и «подписки» взаимодействуют, особенно если есть масса операторов типа map, switchMap, mergeMap, concatMap, catchError и т. д. Иногда хочется вернуться к обычному console.log.
  2. Дебагинг. Если ваш поток проходит через 10 операторов и в конце вы получаете undefined, трудно понять, где именно произошло «обрыв». Помогает разбиение сложного потока на несколько более простых этапов.
  3. Злоупотребление операторами. Соблазн использовать всё и сразу велик. Но иногда достаточно одной функции map вместо трёх разных операторов, усложняющих код.

Практические советы

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

ИТОГ

Реактивное программирование предоставляет мощные инструменты для работы с изменениями, особенно когда существует много зависимостей и асинхронных процессов. Оно помогает писать более чистый код, который в будущем легко расширять и поддерживать. Если вы до сих пор не решались попробовать, стоит дать шанс.

Помните, что это не панацея, и некоторые задачи можно решить проще. Но когда ваше приложение растёт, реактивный подход становится тем мостиком, что позволяет сохранить производительность и понятность кода. Так что читайте документацию, экспериментируйте, и пусть ваш проект засияет новыми цветами!

Рекомендуем публикации по теме