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

Reactive — модель, в которой изменение одной величины автоматически пересчитывает все зависящие от неё значения. В JS реализуется через getter/setter, Proxy или библиотеки типа RxJS.

Идея

Реактивное программирование в основном необходимо для того, чтобы мы моделировали при помощи реакций и вычислений какую-то предметную область — связывать свойства объектов, ячейки таблиц, структуры (деревья, графы, списки).

Главный пример: электронная таблица. Изменил A1 — пересчитались B1, C1 и все формулы, ссылающиеся на A1.

Реактивность через getter/setter

class Cone {
  constructor(h, r1, r2) {
    this.con = { h, r1, r2 };
    this.calculate;
  }
  set h(value) { this.con.h = value; this.calculate; }
  get volume { return this.con.volume; }
  calculate { /* пересчитать всё */ }
}

В setter вызывается полный пересчёт. getter делегирует значения наружу. После каждого изменения переменной, мы вручную вместо того, чтобы посчитать её одну, вызываем метод calculate.

Lazy ≠ reactive

Можно было бы сами вычисления перенести на getter, но тогда бы это не было реактивным программированием.

Lazy (getter) считает только когда запрашивают
Reactive (setter) меняем одно — сразу пересчитываются другие

Реактивность на Proxy

const data = {};
const cell = new Proxy(data, {
  get(obj, key) { return obj[key] ?? ''; },
  set(obj, key, value) {
    if (typeof value === 'function') {
      obj[key] = { fn: value, value: value };
    } else {
      obj[key] = value;
    }
    return true; // ОБЯЗАТЕЛЬНО
  }
});

Прокси — это примерно как getter и setter, только для всех свойств сразу. Универсальный getter/setter.

На выходе из Proxy set обязательно возвращать true, иначе будет TypeError.

Spreadsheet на Proxy

table.D5 = () => max(table.D2, table.D3, table.D4);

Функция кладётся в ячейку. В Proxy set вычисляем значение, храним и формулу, и значение. Чтение возвращает только value.

Хранение плоское — не матрица, а коллекция только заданных ячеек. A0 и AAA9999 — всего две ячейки, не миллион пустых.

Sandbox для упрощения формул

Можно так сделать, чтобы у нас spreadsheet лежал в sandbox, и там внутри были глобальные переменные. Формулы можно писать как max(d2, d3, d4) без префикса table.

Через vm.createScript и runInContext можно вычислять формулы с переменными ячеек как глобалами.

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

Когда формула F5 читает D2, D3, D4 — Proxy get логирует эти обращения. Так можно построить граф зависимостей и при изменении D3 знать, какие формулы пересчитывать.

Пример с диапазоном

К сожалению, такого синтаксиса как D2:D4 мы в JavaScript не добьёмся, но можем добавить функцию range.

sum(...range('D2', 'D5')); // вместо sum(D2:D5)

Где применяется

  • UI: Vue/Solid/Svelte реактивность (Proxy под капотом)
  • MobX: автотрекинг зависимостей и пересчёт
  • Spreadsheet-like приложения
  • Системы автоматизации (датчики → правила → управляющие воздействия)
  • Игры с пересчётом производных параметров

Связанные темы

Ресурсы