Back

Реактивность без фреймворка: что может нативный JS сегодня

Реактивность без фреймворка: что может нативный JS сегодня

Вам нужно реактивное поведение UI — изменения состояния, которые автоматически обновляют DOM, — но вы не хотите добавлять 40 КБ кода фреймворка для простого виджета. Хорошая новость: реактивность на чистом JavaScript вполне достижима с помощью API, которые стабильны в браузерах уже много лет.

В этой статье рассматриваются нативные инструменты, доступные в конце 2025 года для создания реактивных UI: реактивное состояние на основе Proxy, EventTarget и CustomEvent для pub/sub, а также браузерные observers для реакций, учитывающих DOM. Вы узнаете, что работает сегодня, что грядёт, и как эти паттерны соотносятся с внутренним устройством фреймворков.

Ключевые выводы

  • Объекты Proxy перехватывают изменения свойств и обеспечивают автоматическое обновление DOM без зависимостей от фреймворков
  • EventTarget и CustomEvent предоставляют нативный pub/sub слой для слабосвязанного взаимодействия компонентов
  • Браузерные observers (MutationObserver, IntersectionObserver, ResizeObserver) обрабатывают реактивность DOM и layout
  • Предложение TC39 Signals может стандартизировать примитивы реактивности, но текущие паттерны Proxy + EventTarget уже сегодня достигают аналогичных результатов

Что на самом деле означает реактивность

Реактивность — это простой цикл: изменения состояния запускают обновления UI. Фреймворки автоматизируют это с помощью виртуального DOM, компиляторов или детального отслеживания зависимостей. Но базовая механика опирается на возможности JavaScript, которые вы можете использовать напрямую.

Основной паттерн:

  1. Хранить состояние в отслеживаемой структуре
  2. Уведомлять подписчиков при изменении состояния
  3. Обновлять только соответствующие части DOM

Нативные браузерные API обрабатывают каждый шаг без внешних зависимостей.

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

Объект Proxy перехватывает обращения к свойствам и их присваивание. В сочетании с Reflect он формирует основу реактивного состояния на базе прокси.

function createReactiveStore(initial, onChange) {
  return new Proxy(initial, {
    set(target, prop, value) {
      const result = Reflect.set(target, prop, value)
      onChange(prop, value)
      return result
    }
  })
}

const state = createReactiveStore({ count: 0 }, (prop, value) => {
  document.getElementById('count').textContent = value
})

state.count = 5 // DOM обновляется автоматически

Этот паттерн ощущается как “signal-подобный” — вы записываете в состояние, и эффекты выполняются. Система реактивности Vue 3 использует Proxy внутри именно по этой причине.

Ограничение: ловушки Proxy срабатывают только при мутациях, применяемых к самому проксированному объекту. Если вложенные объекты или массивы не обёрнуты также в свои собственные прокси, изменения внутри них (например, array.push()) не будут отслеживаться. Многие разработчики используют иммутабельные обновления (например, state.items = [...state.items, newItem]), чтобы гарантировать срабатывание обновлений.

EventTarget и CustomEvent как pub/sub слой

Для слабосвязанного взаимодействия компонентов EventTarget предоставляет нативный механизм pub/sub. Любой объект может стать источником событий.

const bus = new EventTarget()

// Подписка
bus.addEventListener('state-change', (e) => {
  console.log('Новое значение:', e.detail)
})

// Публикация
bus.dispatchEvent(new CustomEvent('state-change', { 
  detail: { count: 10 } 
}))

Этот паттерн обеспечивает реактивный UI с помощью нативных браузерных API. Компоненты подписываются на события, реагируют на изменения и остаются слабосвязанными. В отличие от кастомных реализаций pub/sub, EventTarget интегрируется с браузерными DevTools и следует стандартной семантике событий.

Браузерные observers для реактивности DOM

Когда нужно реагировать на изменения DOM или layout — а не только на состояние — браузерные observers заполняют этот пробел.

MutationObserver отслеживает модификации DOM:

const observer = new MutationObserver((mutations) => {
  mutations.forEach(m => console.log('DOM изменён:', m))
})
observer.observe(document.body, { childList: true, subtree: true })

IntersectionObserver отслеживает видимость элементов — полезно для ленивой загрузки или аналитики.

ResizeObserver реагирует на изменения размера элементов без polling.

Эти API давно стабильны и безопасны для production. Они дополняют реактивность, управляемую состоянием, обрабатывая случаи, когда внешние факторы модифицируют DOM.

Предложение TC39 Signals: что грядёт

Растёт интерес к стандартизации примитивов реактивности. Предложение TC39 Signals нацелено на определение общей модели, которую могли бы разделять фреймворки.

Важно: по состоянию на 2025 год это всё ещё предложение — не реализованная возможность JavaScript. Фреймворки вроде Solid, Angular и Preact приняли signal-подобные паттерны, влияя на дизайн предложения. Но вы не можете использовать “нативные signals” в браузерах сегодня.

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

Выбор правильного паттерна

ПаттернЛучше всего дляКомпромисс
ProxyЛокальное состояние компонентаОтслеживает изменения только проксированного объекта, если вложенные значения также не проксированы
EventTargetМежкомпонентный обмен сообщениямиРучная настройка связей
MutationObserverРеакция на внешние изменения DOMНакладные расходы на производительность

Для небольших приложений и виджетов комбинация состояния на основе Proxy с EventTarget покрывает большинство потребностей реактивного UI без накладных расходов фреймворка.

Заключение

Реактивность без фреймворка практична уже сегодня. Proxy обрабатывает отслеживание состояния, EventTarget предоставляет pub/sub, а браузерные observers реагируют на изменения DOM. Эти API стабильны, хорошо документированы и компонуются в лёгкое реактивное ядро.

Вам не нужен фреймворк для получения детальной реактивности. Вам нужно понимать примитивы, на которых построены фреймворки — и теперь вы их понимаете.

Часто задаваемые вопросы

Ловушки Proxy срабатывают только при прямом присваивании свойств проксированному объекту. Для вложенных объектов нужно либо рекурсивно обернуть каждый вложенный объект в свой собственный Proxy, либо заменять всю вложенную структуру при внесении изменений. Большинство разработчиков выбирают паттерны иммутабельных обновлений, такие как spread, для создания новых ссылок.

EventTarget — это нативный браузерный API, который интегрируется с DevTools и следует стандартной семантике dispatch. Полное всплытие (bubbling) и захват (capturing) применяются только когда цель события является частью дерева DOM. Кастомные библиотеки могут предлагать дополнительные возможности, такие как wildcard-слушатели или одноразовые подписки, но EventTarget не требует зависимостей и работает согласованно во всех современных браузерах.

Используйте MutationObserver, когда нужно реагировать на изменения DOM, сделанные внешним кодом, сторонними скриптами или расширениями браузера. Proxy отслеживает изменения состояния JavaScript, которые вы контролируете. MutationObserver наблюдает за самим деревом DOM независимо от того, что вызвало изменение. Они служат разным целям и часто работают вместе.

Предложение Signals нацелено на стандартизацию примитивов реактивности, которые могут разделять фреймворки, а не на замену существующих API. Proxy и EventTarget останутся валидными подходами. Если Signals будут реализованы, они, вероятно, дополнят эти паттерны, предоставляя стандартный интерфейс для детального отслеживания зависимостей между различными библиотеками.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay