Учебник веб-разработки
Разделы учебника
На этой странице

IV. React и состояние

Реактивность и FRP: значения во времени

Оглавление · FP · Flux и Context

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

Значение и событие различаются

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

Для модели времени Time сигнал можно представить отображением b: Time → A. События — упорядоченной последовательностью пар (time, value). Это упрощённая модель для рассуждения: она не обещает бесконечную память, непрерывное вычисление или точные вещественные часы в JavaScript.

В раннем FRP behaviors описывали изменяющиеся во времени значения, а events — события с данными; модель имела явную семантику времени. Functional Reactive Animation, Elliott и Hudak. В разных современных системах детали отличаются, поэтому FRP не используем как синоним любых подписок, observer или автоматического перерендера.

Преобразование и накопление

Если f: A → B, сигнал можно преобразовать по точкам: map(f, b)(t) = f(b(t)). Чтобы из событий получить состояние, нужен начальный элемент и правило перехода. После k-го события: state[k + 1] = step(state[k], event[k]).

Исходник схемы
flowchart LR
  Events["Последовательность выборов"] --> Fold["Начальное значение и step"]
  Fold --> State["Текущая площадка"]
  State --> Visible["Производное расписание"]
  Entries["Исходные события расписания"] --> Visible

Стрелки показывают вычислительные зависимости. Один ряд — действия пользователя, другой вход — записи фестиваля. Одинаковое слово «событие» не делает их одной моделью.

Небольшая дискретная модель hold сохраняет последнее значение события. Это учебное вычисление по готовому списку, не реализация FRP-библиотеки:

type Occurrence<T> = { readonly tick: number; readonly value: T };

export function holdAt<T>(initial: T, events: readonly Occurrence<T>[], tick: number): T {
  let value = initial;
  for (const event of events) {
    if (event.tick > tick) break;
    value = event.value;
  }
  return value;
}

const selections: readonly Occurrence<string>[] = [
  { tick: 1, value: "Площадь" },
  { tick: 3, value: "Шатёр" },
  { tick: 3, value: "Площадь" },
];
console.log(holdAt("", selections, 0)); // ""
console.log(holdAt("", selections, 2)); // "Площадь"
console.log(holdAt("", selections, 3)); // "Площадь": последняя запись этого tick

Контракт: конечные неотрицательные целые tick, список отсортирован по tick, для равного tick выбран порядок элементов списка. Он не проверяется типом. Если список не отсортирован, ранний break делает результат неверным. Настоящая система должна договориться о порядке одновременных событий, времени наблюдения и доставке; формула сама этих решений не принимает.

Граф зависимостей

У фильтра есть источники entries, query, location и производные normalizedQuery, visible, count:

Исходник схемы
flowchart LR
  Query["query"] --> Normalized["normalizedQuery"]
  Normalized --> Visible["visible"]
  Location["location"] --> Visible
  Entries["entries"] --> Visible
  Visible --> Count["count"]
  Visible --> List["список интерфейса"]

Одна запись visible определяет и список, и count. Если независимо сохранять оба результата и обновлять их разными обработчиками, можно временно получить новый список со старым числом. Такое несогласованное наблюдение производных часто называют glitch. Граф показывает зависимость, но сам не задаёт порядок обновлений, транзакцию или правила публикации снимка.

В нашей React-лаборатории список и число вычисляются из одного visible в одном чистом рендере. В библиотеке с графом нужны её правила согласованности: когда вычисляются узлы и что наблюдатель может увидеть между изменениями. Нельзя обещать отсутствие glitches только по названию подхода.

Несколько способов организовать обновление

МодельЧто задаёт разработчикЧто важно выяснить
React state/reducerВладелец state, события, вычисление дереваСнимки, ключи, границы компонента
Flux-подобный потокСобытия и правила изменения владельцаПорядок действий, место эффекта
MobXObservable state, actions, computed и reactionsКакие чтения отслеживаются, срок жизни reactions
АтомыНебольшие единицы состояния и связиОбласть владельца, производные атомы, согласованность
СигналыЧитаемые изменяемые значения и вычисленияОтслеживание зависимостей, планирование и интеграция UI
Потоки событийПоследовательности, операторы и подпискиПовторы, отмена, ошибки, буферизация и очистка

Это семейства механизмов, а не взаимоисключающие архитектуры. Например, MobX также использует направленный поток от actions через state к производным. У атомов и сигналов нет единого API или общего контракта всех библиотек.

MobX отслеживает чтения observable в tracked-вычислениях. Computed выводит значение, reaction выполняет эффект. Для React-интеграции используется observer. Поэтому «создать observable объект» и «подключить его к обновлению интерфейса» — разные шаги. Динамические зависимости определяются фактически прочитанными значениями, а не только текстом объявления функции.

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

Запросы во времени

При изменении поисковой строки возможны разные политики:

  • Выполнять все запросы и принимать каждый результат.
  • Выполнять все, но применять только результат актуальной попытки.
  • При новом запросе также просить предыдущую операцию отмениться.
  • Сначала ждать паузу ввода, затем запускать запрос.

Это разные решения. Debounce уменьшает число запусков при частом вводе, но не гарантирует правильного порядка ответов. Отписка от результата не обязательно отменяет транспорт. Даже отмена транспорта не откатывает запись. В автомате попыток отдельно проверялся номер, в эффектах — срок принятия результата после cleanup.

Для синхронной фильтрации небольшого массива ожидание паузы может лишь добавить задержку. В Atmanki поиск учебника использует локальные тексты: сетевой поток для него не нужен.

Сравнительная лаборатория

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

В каждой реализации покажем владельца, вход изменения, вычисление результата, подписку и её завершение. Сравним проверки и объём кода; счётчик рендеров будет измерением конкретной реализации, не универсальной оценкой библиотеки. Исполняемая лаборатория сравнивает reducer, MobX 7.0.6 и учебные атомы/сигналы, включая cleanup и владельцев URL/draft/persistence. Библиотеки не подключаются к основному сайту. Обновление входного набора, два экземпляра и асинхронное расширение — дополнительные задания читателю.

Практика и самопроверка

Сохраните TS-блок как временный lesson.mts в своей ветке и запустите Node. Проверьте пустой ряд, время до первого события, между событиями и после последнего. Поменяйте порядок двух записей с tick 3: значение для tick 3 должно измениться. Затем переставьте события разных tick и покажите нарушение контракта сортировки.

Нарисуйте зависимости FilterLab из главы Flux. Добавьте желание запоминать URL: обозначьте направление синхронизации и политику истории браузера до выбора библиотеки. Удалите временный файл.

Объясните различия между событием, текущим значением, производным значением и эффектом. Почему граф зависимостей не доказывает порядок сетевых ответов?