⚡ Реактивность
В современной фронтенд-разработке реактивность стала стандартом дефакто. Мы привыкли к ней в Vue, React (если через хуки/стейт — здесь появляется спорный момент), Angular (Signals) и RxJS. Однако тяжелые фреймворки не всегда уместны, если вам нужно построить чистую, легковесную бизнес-логику, абстрагированную от DOM-дерева.
Вот что я хочу Вам показать: https://www.npmjs.com/package/@pravosleva/reactive-engine
NPM-пакет @pravosleva/reactive-engine решает именно эту задачу, предоставляя разработчикам специализированный движок (Reactive Engine) для управления реактивными состояниями и автоматического вычисления зависимостей на чистом JavaScript / TypeScript.
🎯 Основная идея инструмента
Архитектура @pravosleva/reactive-engine базируется на концепции направленного графа вычислений. Вместо того чтобы вручную вызывать функции обновления при каждом изменении переменной, вы описываете связи между данными один раз. Движок сам берет на себя задачу по цепочке обновить все зависимые узлы.
Ключевые преимущества такого подхода:
-
Разделение ответственности (Decoupling): Бизнес-логика полностью отделена от слоя представления (UI-рендеринга).
-
Минимизация лишних вычислений: Движок кэширует промежуточные результаты и пересчитывает только то, что действительно изменилось.
-
Предсказуемый Data Flow: Потоки данных движутся строго в одном направлении, исключая бесконечные циклы обновлений.
🛠️ Как это устроено под капотом?
Подобные реактивные движки обычно реализуют три фундаментальных типа сущностей:
-
Атомы / Сигналы (Observables / Atoms) — базовые ячейки хранения данных. Они хранят примитивы или объекты и генерируют событие всякий раз, когда их значение перезаписывается.
-
Вычисляемые свойства (Computed / Computed Nodes) — узлы графа, которые зависят от других ячеек. Они лениво (lazy) вычисляют свое значение и автоматически подписываются на те атомы, которые были прочитаны в процессе выполнения их функции.
-
Эффекты / Реакции (Effects / Reactions) — конечные точки графа, которые не возвращают новых данных, но выполняют побочные эффекты (Side Effects): сохраняют данные в LocalStorage, отправляют API-запросы или дергают методы обновления интерфейса.
💻 Концептуальный пример использования (Чистый JS)
Архитектура @pravosleva/reactive-engine базируется на концепции направленного графа вычислений. Вместо того чтобы вручную вызывать функции обновления при каждом изменении переменной, вы описываете связи между данными один раз через свойство .value. Движок сам берет на себя задачу по цепочке обновить все зависимые узлы.
Давайте представим стандартную задачу: расчет стоимости корзины товаров с учетом динамической скидки и отправкой аналитики. С использованием реактивного движка на чистом JS код принимает декларативный вид:
import { ReactiveEngine } from '@pravosleva/reactive-engine';// 1. Инициализируем движок и базовые состояния (Сигналы)const engine = new ReactiveEngine();const price = engine.signal(1000);const quantity = engine.signal(2);const discountPercent = engine.signal(10); // 10%// 2. Описываем вычисляемые узлы графа (Computed)// Движок автоматически отслеживает, к каким .value было обращение внутри функцийconst baseTotalPrice = engine.computed(() => price.value * quantity.value);const finalPrice = engine.computed(() => { const total = baseTotalPrice.value; const discount = (total * discountPercent.value) / 100; return total - discount;});// 3. Создаем побочный эффект (Effect)engine.effect(() => { console.log(`[UI Update] Итоговая сумма к оплате: ${finalPrice.value} руб.`);});// --- Работа с реактивным графом ---// В консоли сразу сработает эффект при инициализации:// "[UI Update] Итоговая сумма к оплате: 1800 руб."// Меняем количество товара (Операция ++ вызывает сеттер свойства .value)quantity.value++;// Движок автоматически пересчитает baseTotalPrice -> finalPrice -> вызовет эффект:// В консоли: "[UI Update] Итоговая сумма к оплате: 2700 руб."
💻 Реальный пример использования в React
В библиотеке @pravosleva/reactive-engine реактивные сущности (сигналы) предоставляют доступ к своему состоянию через свойство .value. Благодаря встроенным JavaScript-геттерам и сеттерам, изменение значения через конструкцию .value++ автоматически перехватывается движком и запускает цепочку обновлений.
Для связки с UI-слоем React библиотека предоставляет удобный хук useReactiveValue, который подписывает компонент на изменения (но можно и без него — далее по тексту)
1. Создание хранилища (`store.ts`)
import { createEngine } from '@pravosleva/reactive-engine';const engine = createEngine();// Базовая ячейка состояния (Сигнал)export const counterSignal = engine.signal(0);// Вычисляемый узел графа (Computed). Автоматически зависит от counterSignal.export const doubleComputed = engine.computed(() => counterSignal.value * 2);// Побочный эффект (Effect). Срабатывает при каждом изменении counterSignal.engine.effect(() => { console.log(`[Лог] Текущий счетчик изменился: ${counterSignal.value}`);});
2. Интеграция в React-компонент (`Counter.tsx`)
import React from 'react';import { useReactiveValue } from '@pravosleva/reactive-engine';import { counterSignal, doubleComputed } from './store';export const Counter = () => { // Хук автоматически подпишется на изменения и вызовет ререндер const count = useReactiveValue(counterSignal); const doubleCount = useReactiveValue(doubleComputed); return ( <div style={{ padding: 20, border: '1px solid #ccc', borderRadius: 8 }}> <h3>Счетчик: {count}</h3> <p>Удвоенное значение (Computed): {doubleCount}</p> {/* Операция ++ разворачивается в counterSignal.value = counterSignal.value + 1 */} {/* Сеттер свойства .value перехватывает это действие и обновляет граф */} <button onClick={() => counterSignal.value++}>Увеличить</button> <button onClick={() => counterSignal.value--}>Уменьшить</button> </div> );};
3. Вариант MobX like (для тех кто не любит хуки):
import React from 'react';import { observer } from '@pravosleva/reactive-engine';import { counterSignal, userSignal } from './store';// Оборачиваем компонент в observer hoc.// Теперь можно просто читать `.value` прямо в JSX — никаких хуков не требуется!export const ProfileDashboard = observer(() => { return ( <div style={{ padding: 20, border: '1px solid #aaa' }}> <h3>Пользователь: {userSignal.value.name}</h3> <p>Значение счетчика: {counterSignal.value}</p> <button onClick={() => counterSignal.value++}>Увеличить</button> <button onClick={() => { userSignal.value = { name: 'Пётр' }; }}> Сменить имя </button> </div> );});
4. Еще один вариант с `<Observer>` (подойдет для риалтайма в огромных дашбордах)
import React, { useRef } from 'react';import { createObserverComponent, engine } from '@pravosleva/reactive-engine';// Инициализируем компонент-контейнер из нашей фабрикиconst Observer = createObserverComponent(engine);// Сигнал, который меняется очень часто (например, каждую секунду по веб-сокету)const livePriceSignal = engine.signal(100);export const MassiveDashboard = () => { // Счетчик рендеров всего ОГРОМНОГО компонента const totalDashboardRenders = useRef(0); totalDashboardRenders.current++; return ( <div style={{ padding: '20px', border: '1px solid #ccc' }}> <h2>📊 Тяжелая панель аналитики</h2> <p>Рендеров всей страницы: {totalDashboardRenders.current}</p> {/* Тяжелый статический контент, который генерируется долго. Благодаря инлайн-контейнеру <Observer>, этот кусок НИКОГДА не будет перерисовываться при изменении цены! */} <div className="heavy-charts-and-tables"> <p>...Тут рендерятся 10 тяжелых графиков и таблиц...</p> </div> <hr /> {/* ТОЧЕЧНАЯ РЕАКТИВНОСТЬ: Оборачиваем в <Observer> только ту микро-зону, которая зависит от сигнала. При изменении livePriceSignal.value перерисовываться будет СТРОГО анонимная функция внутри <Observer>, экономя 99% ресурсов процессора! */} <Observer> {() => { const innerCounter = useRef(0); innerCounter.current++; return ( <div style={{ background: '#f9f9f9', padding: '10px' }}> <h3>📈 Живой график цены (Текущая: ${livePriceSignal.value})</h3> <p style={{ color: 'green' }}> Рендеров этой микро-зоны: {innerCounter.current} </p> </div> ); }} </Observer> <button onClick={() => { livePriceSignal.value += 5; }}> Симулировать изменение цены (+5$) </button> </div> );};
📦 Установка и интеграция
Пакет поставляется в виде легковесного scoped-модуля и легко интегрируется в любой стек разработки (Node.js, Vanilla JS, Webpack/Vite сборки):
yarn add @pravosleva/reactive-engine
Благодаря тому, что движок является headless (не привязан к конкретной среде рендеринга), его можно одинаково эффективно использовать:
-
На бэкенде в Node.js для создания сложных калькуляторов тарифов или игровых движков.
-
Во фронтенд-микрофронтендах для создания единой шины данных, синхронизированной между разными фреймворками.
-
В Canvas/WebGL анимациях для быстрого реагирования на изменение координат объектов.
📋 Резюме
Пакет @pravosleva/reactive-engine — это отличный выбор для разработчиков, которым нужен полный контроль над реактивностью без необходимости тащить за собой тяжеловесные экосистемы вроде MobX или RxJS. Он позволяет структурировать хаотичные потоки данных в понятный, легко тестируемый и высокопроизводительный вычислительный граф.
Более подробная дока доступна в исходниках:
— 🇷🇺 На русском
— 🇬🇧 На английском
Большую часть кейсов закрыл тестами, можно также их посмотреть в исходниках
Конструктивная критика категорически приветствуется ) Можно здесь в комментах либо в отдельный чатик
ссылка на оригинал статьи https://habr.com/ru/articles/1057558/