
Каждому разработчику приложений знакома проблема управления состоянием. Представьте: у вас есть часть состояния — объект пользователя, настройка темы, — к которой должен обращаться компонент, находящийся глубоко в дереве вашего интерфейса. Традиционный подход — «пробрасывание пропсов»: передача данных через каждый промежуточный компонент. Это утомительно, чревато ошибками и создает тесную связь между компонентами, которые не должны знать о существовании друг друга.
Современные фреймворки решают эту проблему с помощью «Context API» — центрального провайдера, который делает состояние доступным для любого дочернего компонента. Это, хотя и решает проблему пробрасывания пропсов, зачастую приводит к скрытым проблемам с производительностью. Во многих реализациях, когда любое значение в контексте изменяется, все компоненты, использующие этот контекст, вынуждены перерисовываться, даже если их не касается конкретная изменившаяся часть данных.
Но если вдуматься, проблема с производительностью — не неизбежное зло, а результат выбора архитектурного решения. А оно может быть простым: переместить управление состояниями за пределы основного потока.
Таков фундаментальный принцип платформы Neo.mjs. Важно лишь понять основную концепцию ее архитектуры — это отправная точка работы с платформой: в Neo.mjs провайдеры состояния (State Providers), включая все их реактивные данные и формулы, существуют и выполняются исключительно внутри веб-воркера.
Вот как это реализуется в Neo.mjs: платформа изначально предлагает целостную многопоточную среду выполнения. State Provider — не просто внешнее хранилище, которое можно добавить, а базовая возможность самой платформы. Именно поэтому можно воспользоваться всеми удобствами Context API без ущерба для производительности.
Далее вы узнаете, как создать State Provider с нуля, чтобы получить систему, безупречную во всех отношениях, — интуитивную, стабильную и производительную.
Эволюция Neo.mjs: от пользовательского парсинга к универсальному подходу
В Neo.mjs State Provider существовал давно и изначально был реактивным. Так в чем же принципиальное отличие версии v10? Все дело в фундаментальном подходе.
Предыдущий State Provider был продуманной системой, разработанной под конкретные задачи. Он работал за счет парсинга функций привязки с помощью регулярных выражений для определения используемых свойств data. Этот подход был эффективным, но имел два серьезных ограничения:
- Работал только с
data: Позволял привязываться только к свойствам внутри объектаdataсамого провайдера. Привязка к свойствуcountвнешнего хранилища илиwidthдругого компонента была просто невозможна.
- Был нестабильным: Поскольку система полагалась на парсинг через регулярные выражения, сложные или нестандартно оформленные функции привязки иногда не регистрировали зависимости корректно, что приводило к сложным поискам ошибок.
Революционное значение v10 заключалось в отказе от кастомной логики парсинга и перестройке всей системы управления состоянием на основе универсального фундаментального подхода: Neo.core.Effect.
Этот фундаментальный подход и придает современному state.Provider такую эффективность. Ему больше не нужно угадывать зависимости — теперь он их знает. Когда выполняется функция привязки, core.Effect отслеживает каждое используемое реактивное свойство — независимо от его источника — и в реальном времени строит точный граф зависимостей.
В итоге получаем API, который не только эффективнее, но и проще, и интуитивнее — особенно когда дело доходит до изменений состояния. Провайдер работает ожидаемо, автоматически обрабатывая даже сложные сценарии, например, глубокое слияние объектов.
Настоящая магия начинается, когда изменяются данные. Новая система реактивности на основе прокси, которая отслеживает вложенные свойства, позволяет менять состояние с помощью обычных операторов присваивания. Выполнить это проще уже невозможно:
// Получаем провайдер состояния и изменяем данные напрямую.
const provider = myComponent.getStateProvider();
// Всего одна строка - и реактивное обновление запущено.
provider.data.user.firstname = 'Max';
// Не перезаписывает фамилию
provider.setData({user: {firstname: 'Robert'}})
// Можно обновить несколько свойств одновременно. Благодаря автоматическому группированию
// это приводит всего к одному циклу обновления UI.
provider.setData({user: {firstname: 'John', lastname: 'Doe'}})
// Альтернативный синтаксис:
provider.setData({
'user.firstname': 'John',
'user.lastname' : 'Doe'
});
Рассмотрим реальный пример. Ниже представлена интерактивная демонстрация того, как компонент может связывать и изменять состояние из провайдера.
import Button from 'neo.mjs/src/button/Base.mjs';
import Container from 'neo.mjs/src/container/Base.mjs';
import Label from 'neo.mjs/src/component/Label.mjs';
class MainView extends Container {
static config = {
className: 'My.StateProvider.Example1',
stateProvider: {
data: {
user: {
firstName: 'Tobias',
lastName : 'Uhlig'
}
}
},
layout: {ntype: 'vbox', align: 'start'},
items: [{
module: Label,
bind: {
text: data => `User: ${data.user.firstName} ${data.user.lastName}`
},
style: {marginBottom: '10px'}
}, {
module: Button,
text: 'Change First Name',
handler() {
// Выполняет ГЛУБОКОЕ СЛИЯНИЕ, а не перезапись.
// Свойство 'lastName' будет сохранено.
this.setState({
user: { firstName: 'John' }
});
}
}, {
module: Button,
text: 'Change Last Name (Path-based)',
style: {marginTop: '10px'},
handler() {
// Также можно установить значение, используя его путь.
this.setState({'user.lastName': 'Doe'});
}
}]
}
}
export default Neo.setupClass(MainView);
Обратите внимание на кнопку «Change First Name» («Изменить имя»). Она вызывает setState, передавая объект только со свойством firstName. Провайдер в v10 достаточно интеллектуален, чтобы выполнить глубокое слияние: он обновляет firstName, оставляя lastName без изменений. Это предотвращает случайную потерю данных и делает обновления состояния по умолчанию безопасными и предсказуемыми.
Сила формул: получение производных состояний без усилий
Поскольку провайдер создан на основе Neo.core.Effect, вычисляемые свойства («формулы») являются встроенной и полноценной возможностью. Вы определяете их в отдельной конфигурации formulas, а провайдер автоматически поддерживает их в актуальном состоянии.
import Container from 'neo.mjs/src/container/Base.mjs';
import Label from 'neo.mjs/src/component/Label.mjs';
import TextField from 'neo.mjs/src/form/field/Text.mjs';
class MainView extends Container {
static config = {
className: 'My.StateProvider.Example2',
layout: {ntype: 'vbox', align: 'stretch'},
stateProvider: {
data: {
user: {
firstName: 'Tobias',
lastName : 'Uhlig'
}
},
formulas: {
fullName: data => `${data.user.firstName} ${data.user.lastName}`
}
},
items: [{
module: Label,
bind: { text: data => `Welcome, ${data.fullName}!` },
style: {marginBottom: '10px'}
}, {
module: TextField,
labelText: 'First Name',
bind: { value: data => data.user.firstName },
listeners: {
change: function({value}) { this.setState({'user.firstName': value}) }
}
}, {
module: TextField,
labelText: 'Last Name',
bind: { value: data => data.user.lastName },
listeners: {
change: function({value}) { this.setState({'user.lastName': value}) }
}
}]
}
}
export default Neo.setupClass(MainView);
При редактировании текстовых полей вызов setState обновляет базовые данные user. Система Effect обнаруживает это изменение, автоматически пересчитывает формулу fullName и обновляет приветственную надпись.
Формулы в контексте иерархичности
Истинная мощь иерархической системы раскрывается, когда формулы в дочернем провайдере могут бесшовно использовать данные из родительского. Это позволяет создавать мощные вычисления в области видимости, которые при этом остаются реактивными к глобальному состоянию приложения.
import Button from 'neo.mjs/src/button/Base.mjs';
import Container from 'neo.mjs/src/container/Base.mjs';
import Label from 'neo.mjs/src/component/Label.mjs';
class MainView extends Container {
static config = {
className: 'My.StateProvider.Example4',
layout: {ntype: 'vbox', align: 'stretch', padding: '10px'},
// 1. Родительский провайдер с глобальной налоговой ставкой.
stateProvider: {
data: {
taxRate: 0.19
}
},
items: [{
module: Label,
bind: { text: data => `Global Tax Rate: ${data.taxRate * 100}%` }
}, {
module: Button,
text: 'Change Tax Rate',
handler() {
this.setState({taxRate: Math.random().toFixed(2)});
},
style: {marginBottom: '10px'}
}, {
module: Container,
// 2. Дочерний провайдер с локальной ценой.
stateProvider: {
data: {
price: 100
},
formulas: {
// 3. Эта формула использует данные из ОБОИХ провайдеров.
totalPrice: data => data.price * (1 + data.taxRate)
}
},
style: {padding: '10px'},
layout: {ntype: 'vbox', align: 'start'},
items: [{
module: Label,
bind: { text: data => `Local Price: €${data.price.toFixed(2)}` }
}, {
module: Label,
bind: { text: data => `Total (inc. Tax): €${data.totalPrice.toFixed(2)}` },
style: {fontWeight: 'bold', marginTop: '10px'}
}, {
module: Button,
text: 'Change Price',
handler() {
this.setState({price: Math.floor(Math.random() * 100) + 50});
},
style: {marginTop: '10px'}
}]
}]
}
}
export default Neo.setupClass(MainView);
В данном примере формула totalPrice в дочернем провайдере зависит как от собственного свойства price, так и от родительского taxRate. Нажатие любой из кнопок запускает корректное реактивное обновление, и итоговая стоимость всегда остается синхронизированной. Это наглядно демонстрирует, как легко можно комбинировать состояние из разных частей приложения.
Спроектированная иерархия: вложенные провайдеры, работающие просто и эффективно
Провайдер версии v10 был спроектирован для работы с состояниями разного уровня через интеллектуальную иерархическую модель. Дочерний компонент может беспрепятственно получать данные как от собственного провайдера, так и от любого родительского.
import Container from 'neo.mjs/src/container/Base.mjs';
import Label from 'neo.mjs/src/component/Label.mjs';
class MainView extends Container {
static config = {
className: 'My.StateProvider.Example3',
stateProvider: {
data: { theme: 'dark' }
},
layout: {ntype: 'vbox', align: 'stretch', padding: '10px'},
items: [{
module: Label,
bind: { text: data => `Global Theme: ${data.theme}` }
}, {
module: Container,
stateProvider: {
data: { user: 'Alice' }
},
style: {padding: '10px', marginTop: '10px'},
items: [{
module: Label,
bind: {
text: data => `Local User: ${data.user} (Theme: ${data.theme})`
}
}]
}]
}
}
export default Neo.setupClass(MainView);
Вложенный компонент может получать данные как из своего локального провайдера (user), так и из родительского (theme), без необходимости дополнительной настройки.
Провайдеры состояния в функциональных компонентах
Благодаря рефакторингу в v10, провайдеры состояния теперь стали полноценной частью функциональных компонентов. Вы можете определить провайдер и привязаться к его данным с той же эффективностью и простотой, что и в компонентах на основе классов.
import {defineComponent} from 'neo.mjs';
import Label from 'neo.mjs/src/component/Label.mjs';
import TextField from 'neo.mjs/src/form/field/Text.mjs';
export default defineComponent({
stateProvider: {
data: {
user: {
firstName: 'Jane',
lastName : 'Doe'
}
},
formulas: {
fullName: data => `${data.user.firstName} ${data.user.lastName}`
}
},
createVdom(config) {
return {
layout: {ntype: 'vbox', align: 'stretch'},
items: [{
module: Label,
bind: { text: data => `Welcome, ${config.data.fullName}!` },
style: {marginBottom: '10px'}
}, {
module: TextField,
labelText: 'First Name',
bind: { value: data => config.data.user.firstName },
listeners: {
change: ({value}) => config.setState({'user.firstName': value})
}
}, {
module: TextField,
labelText: 'Last Name',
bind: { value: data => config.data.user.lastName },
listeners: {
change: ({value}) => config.setState({'user.lastName': value})
}
}]
}
}
});
Этот пример демонстрирует всю мощь новой архитектуры: функциональный компонент с собственными реактивными данными, вычисляемыми свойствами и двусторонним связыванием — все это реализовано в чистом декларативном коде.
Как это устроено: прокси и «распространение реактивности»
Элегантный API, представленный выше, работает благодаря сложной системе прокси, создаваемой Neo.state.createHierarchicalDataProxy. Взаимодействуя с provider.data, вы работаете не с обычным объектом, а с интеллектуальным агентом, который взаимодействует с EffectManager платформы Neo.mjs.
- Ловушка
get: Когда функция привязки выполняется, ловушкаgetв прокси перехватывает обращения к свойствам и сообщаетEffectManager: «The currently running effect depends on this property» («Текущий выполняемый эффект зависит от этого свойства»). Это автоматически строит граф зависимостей.
Это ключ к разработке без шаблонного кода. В отличие от других систем, где зависимости нужно объявлять вручную, рискуя столкнуться с устаревшими замыканиями или бесконечными циклами, Neo.mjs обнаруживает их автоматически, просто отслеживая, какие данные читает ваш код.
- Ловушка
set: Когда выполняется присваивание, например,provider.data.user.firstname = 'Max', ловушка set в прокси перехватывает операцию. Затем она вызывает внутренний метод провайдераsetData(), который активирует систему реактивности, чтобы перезапустить только те эффекты, которые зависят от этого конкретного свойства.
Этот прокси служит мостом между простым опытом разработчика и мощным, детализированным реактивным механизмом. Он также обеспечивает ключевую функцию, которую мы называем «распространение реактивности». Изменение в свойстве-листе (например, user.name) корректно воспринимается как изменение его родительского объекта (user), гарантируя обновление компонентов, привязанных к родительскому объекту.
Заключение: реактивность как основа архитектуры
Новый state.Provider — это не просто инструмент управления состоянием, а прямое воплощение основной философии фреймворка. Построенный на фундаменте истинной, детализированной реактивности, он предлагает систему, которая:
- интуитивна: изменяет состояние как обычный JavaScript. API — чистый, прямой и без шаблонного кода.
- безупречно производительна: обновляются только компоненты, зависящие от конкретных измененных данных. «Налог контекста» устранен по умолчанию.
- предсказуема и надежна: благодаря таким функциям, как «распространение реактивности», система ведет себя именно так, как ожидает разработчик, устраняя скрытые подводные камни и делая управление состоянием надежным и комфортным процессом.
В этом и заключается революционность новой версии платформы Neo.mjs: высокопроизводительная архитектура, которая обеспечивает более простой, продуктивный и приятный опыт разработки. Вы тратите время на создание функций, а не на борьбу с инструментами.
Лучше один раз увидеть
Читать о производительности — одно дело, а оценить ее — совсем другое. Поскольку примеры кода в этой статье статичны, можете ознакомиться с реальными интерактивными версиями и начать работу с Neo.mjs за считанные минуты.
1. Интерактивные примеры: Изучите код из этой статьи и более 70 других примеров в живом исполнении на нашем портале примеров. Можете редактировать код прямо в браузере и мгновенно видеть результаты.
=> перейти к примерам портала
2. Создайте свое первое приложение: Скрипт create-app — это самый быстрый способ запустить многопоточное приложение «Hello World» на вашем компьютере.
Читайте также:
- Совместное использование данных WebSocket в режиме реального времени в нескольких окнах браузера
- Производительность фронтенда: лав-стори для разработчиков
- Как создавать сайты с молниеносной загрузкой: рекомендации по оптимизации фронтенда. Часть 2
Читайте нас в Telegram, VK и Дзен
Перевод статьи Tobias Uhlig: Designing a State Manager for Performance: A Deep Dive into Hierarchical Reactivity





