Здравствуйте, меня зовут Дмитрий Карловский, и я.. тот самый чел, который написал реактивную библиотеку $mol_wire. Именно благодаря мне вам есть сейчас чем пугать детей перед сном.
Но просто написать классную библиотеку - слишком мелкая цель. Построить на ней богатый фреймворк с кучей батареек - уже интересней, но всё ещё не достаточно амбициозно. Разработанный мной подход может стать lingua franca в коммуникациях между библиотеками, состояниями браузера, и даже между удалёнными узлами.
Берегите синапсы, сейчас будет настоящий киберпанк..
![](https://habrastorage.org/webt/tj/nd/ao/tjndaoxz6rg-tvia9kzbdwnzsry.jpeg)
Реактивный React
ReactJS сейчас самый популярный фреймворк, вопреки множеству архитектурных просчётов. Вот лишь некоторые из них:
Компонент не отслеживает внешние состояния, от которых он зависит, — обновляется он только при изменении локального. Это требует аккуратных подписок/отписок и своевременных уведомлений об изменениях.
Единственный способ изменить один параметр компонента — это полностью ререндерить внешний компонент, заново сформировав все параметры как для него, так и для соседей. То же касается и добавления/удаления/перемещения компонента.
При перемещении компонента между контейнерами происходит его полное пересоздание. И наоборот, разные экземпляры одного компонента могут быть неуместно реиспользованы.
Создаваемые на лету колбэки приводят к лишним ререндерам, поэтому требуют аккуратной их мемоизации с точным указанием используемых внутри них переменных.
Хуки нельзя применять в условиях, циклах и других местах динамичности потока исполнения, иначе всё сломается.
Ошибки и индикация ожидания происходят вне компонента. Компонент получается не самодостаточным и фатально влияющим и на внешний компонент и, как следствие, на соседей.
Компонент невозможно обновить частично — только полный ререндер. Чтобы это побороть либо обмазываются мемоизацией, либо излишне увеличивают гранулярность компонент.
Отсутствие контроля stateful компонента часто приводит к необходимости разбивать каждый компонент на два: контролируемый stateless и неконтролируемая stateful обёртка над ним. Частичный контроль при этом сопряжён с трудностями и копипастой.
Что ж, давайте вылечим больного, а заодно покажем простоту интеграции реактивной библиотеки $mol_wire в совершенно инородную ему архитектуру.
![](https://habrastorage.org/webt/us/ly/ea/uslyear0opiathg8whfifakmjue.jpeg)
Начнём издалека — напишем синхронную функцию, которая загружает JSON по ссылке. Для этого напишем асинхронную функцию и конвертируем её в синхронную:
export const getJSON = sync( async function getJSON( uri: string ){
const resp = await fetch(uri)
if( Math.floor( resp.status / 100 ) === 2 ) return resp.json()
throw new Error( `${resp.status} ${resp.statusText}` )
} )
Теперь реализуем API для GitHub, с debounce и кешированием. Поддерживаться у нас будет лишь загрузка данных issue по его номеру:
export class GitHub extends Object {
// cache
@mems static issue( value: number, reload?: "reload" ) {
sleep(500) // debounce
const uri = `https://api.github.com/repos/nin-jin/HabHub/issues/${value}`
return getJSON( uri ) as {
title: string
html_url: string
}
}
}
Сколько бы раз мы ни обращались за данными — результат будет возвращаться из кеша, но если нам потребуется всё же перезагрузить данные — можно передать дополнительный параметр, чтобы запустилась задача по обновлению кеша. В любом случае запрос пойдёт не сразу, а с задержкой в пол секунды.
Теперь, наконец, мы переходим к созданию компонент. Вопреки популярному тренду, мы не будем эмулировать объекты, через грязные функции с хуками, а будем использовать классовые компоненты. А чтобы не повторять одну и ту же логику, создадим базовый класс для наших компонент:
export abstract class Component<
Props = { id: string },
State = {},
SnapShot = any
> extends React.Component<
Partial<Props> & { id: string },
State,
SnapShot
> {
// every component should have guid
id!: string;
// show id in debugger
[Symbol.toStringTag] = this.props.id
// override fields by props to configure
constructor(props: Props & { id: string }) {
super(props)
Object.assign(this, props)
}
// composes inner components as vdom
abstract compose(): any
// memoized render which notify react on recalc
@mem render() {
log("render", "#" + this.id)
Promise.resolve().then(() => this.forceUpdate())
return this.compose()
}
}
Основная идея тут в том, чтобы каждый компонент был полностью самодостаточным, но при этом контролируемым — любое его публичное поле можно переопределить через пропсы. Все пропсы опциональны, кроме идентификатора, который мы требуем задавать извне, чтобы он был глобально уникальным и семантичным.
Важно отметить, что пропсы не отслеживаются реактивной системой — это позволяет передавать в них колбэки и это не будет вызывать ререндеров. Идея тут в том, чтобы разделить инициализацию (через проталкивание пропсов) и собственно работу (путём затягивания через колбэки, предоставленные в пропсах).
Инициализация происходит при конструировании класса, а динамическая работа — когда фреймворк вызывает render
. ReactJS славится тем, что вызывает его слишком часто. Тут же, благодаря мемоизации, мы перехватываем у фреймворка контроль за тем, когда фактически будут происходить ререндеры. Когда поменяется любая зависимость от которой зависит результат рендеринга, реактивная система перевычислит его и уведомит фреймворк о необходимости реконцилиации, тогда фреймворк вызовет render
и получит свежий VDOM. В остальных же случаях он будет получать VDOM из кеша и ничего дальше не делать.
Такая схема работы уже не позволит использовать в своей логике хуки, но с $mol_wire, хуки — как собаке пятая нога.
Проще понять принцип работы на конкретных примерах, так что давайте создадим простой компонент — поле текстового ввода:
export class InputString extends Component<InputString> {
// statefull!
@mem value( next = "" ) {
return next;
}
change( event: ChangeEvent<HTMLInputElement> ) {
this.value( event.target.value )
this.forceUpdate() // prevent caret jumping
}
compose() {
return (
<input
id={ this.id }
className="inputString"
value={ this.value() }
onInput={ action(this).change }
/>
)
}
}
Тут мы объявили состояние, в котором по умолчанию храним введённый текст, и экшен вызывающийся при вводе для обновления этого состояния. В конце экшена мы заставляем ReactJS немедленно подхватить наши изменения, иначе каретка улетит в конец поля ввода. В остальных случаях в этом нет необходимости. Ну а при передаче экшена в VDOM мы завернули его в обёртку, которая просто превращает синхронный метод в асинхронный.
Теперь давайте воспользуемся этим компонентом в поле ввода числа, в который и поднимем состояние поля ввода текста:
export class InputNumber extends Component<InputNumber> {
// self state
@mem numb( next = 0 ) {
return next;
}
dec() {
this.numb(this.numb() - 1);
}
inc() {
this.numb(this.numb() + 1);
}
// lifted string state as delegate to number state!
@mem str( str?: string ) {
const next = str?.valueOf && Number(str)
if( Object.is( next, NaN ) ) return str ?? ""
const res = this.numb( next )
if( next === res ) return str ?? String( res ?? "" )
return String( res ?? "" )
}
compose() {
return (
<div
id={this.id}
className="inputNumber"
>
<Button
id={ `${this.id}-decrease` }
action={ ()=> this.dec() }
title={ ()=> "➖" }
/>
<InputString
id={ `${this.id}-input` }
value={ next => this.str( next ) } // hack to lift state up
/>
<Button
id={ `${this.id}-increase` }
action={ ()=> this.inc() }
title={ ()=> "➕" }
/>
</div>
)
}
}
Обратите внимание, что мы переопределили у поля ввода текста свойство value
, так что теперь оно будет хранить своё состояние не у себя, а в нашем свойстве str
, которое на самом деле является кешированным делегатом уже к свойству numb
. Логика его немного замысловатая, чтобы при вводе не валидного числа, мы не теряли пользовательский ввод из‑за замены его на *нормализованное* значение.
Можно заметить, что сформированный нами VDOM не зависит ни от каких реактивных состояний, а значит он вычислится лишь один раз при первом рендере, и больше обновляться не будет. Но не смотря на это, текстовое поле будет корректно реагировать на изменения свойств numb
и как следствие str
.
Так же тут использованы компоненты Button
у которых переопределены методы, вызываемые для получения названия кнопки и для выполнения действия при клике. Но о кнопках позже, а пока воспользуемся всеми нашими наработками, чтобы реализовать продвинутый Counter
, который не просто переключает число кнопками, но и грузит данные с сервера:
export class Counter extends Component<Counter> {
@mem numb( value = 48 ) {
return value
}
issue( reload?: "reload" ) {
return GitHub.issue( this.numb(), reload )
}
title() {
return this.issue().title;
}
link() {
return this.issue().html_url;
}
compose() {
return (
<div
id={ this.id }
className="counter"
>
<InputNumber
id={ `${this.id}-numb` }
numb={ next => this.numb( next ) } // hack to lift state up
/>
<Safe
id={ `${this.id}-output-safe` }
task={ () => (
<a
id={ `${this.id}-link` }
className="counter-link"
href={ this.link() }
>
{ this.title() }
</a>
) }
/>
<Button
id={ `${this.id}-reload` }
action={ () => this.issue("reload") }
title={ () => "Reload" }
/>
</div>
)
}
}
Как не сложно заметить, состояние текстового поля ввода мы подняли ещё выше — теперь оно оперирует номером issue. По этому номеру мы через GitHub API грузим данные и показываем их рядом, завернув в специальный компонент Safe
, задача которого обрабатывать исключительные ситуации в переданном ему коде: при ожидании показывать соответствующий индикатор, а при ошибке — текст ошибки. Реализуется он просто — обычным try-catch
:
export abstract class Safe extends Component<Safe> {
task() {}
compose() {
try {
return this.task()
} catch( error ) {
if( error instanceof Promise ) return (
<span
id={ `${this.id}-wait` }
className="safe-wait"
>
?
</span>
)
if( error instanceof Error ) return (
<span
id={ `${this.id}-error` }
className="safe-error"
>
{error.message}
</span>
)
throw error
}
}
}
Наконец, реализуем кнопку, но не простую, а умную, умеющую отображать статус выполняемой задачи:
export class Button extends Component<Button> {
title() {
return ""
}
action( event?: MouseEvent<HTMLButtonElement> ) {}
@mem click( next?: MouseEvent<HTMLButtonElement> | null ) {
if( next ) this.forceUpdate()
return next;
}
@mem status() {
const event = this.click()
if( !event ) return
this.action( event )
this.click( null )
}
compose() {
return (
<button
id={this.id}
className="button"
onClick={ action(this).click }
>
{ this.title() } {" "}
<Safe
id={ `${this.id}-safe` }
task={ () => this.status() }
/>
</button>
)
}
}
Тут мы место того, чтобы сразу запускать действие, кладём событие в реактивное свойство click
, от которого зависит свойство status
, которое уже и занимается запуском обработчика события. А чтобы обработчик был вызван сразу, а не в следующем фрейме анимации (что важно для некоторых JS API типа clipboard
), вызывается forceUpdate
. Сам status
в штатных ситуациях ничего не возвращает, но в случае ожидания или ошибки показывает соответствующие блоки благодаря Safe
.
Весь код этого примера можно найти в песочнице:
![](https://habrastorage.org/getpro/habr/upload_files/29f/388/33e/29f38833ef44ca4cb09b85110fa49000.png)
Там добавлены ещё и логи, чтобы можно было понять что происходит. Например, вот так выглядит первичный рендеринг:
render #counter
render #counter-numb
render #counter-numb-decrease
render #counter-numb-decrease-safe
render #counter-numb-input
render #counter-numb-increase
render #counter-numb-increase-safe
render #counter-title-safe
render #counter-reload
render #counter-reload-safe
fetch GitHub.issue(48)
render #counter-title-safe
render #counter-title-safe
Тут #counter-title-safe
рендерился 3 раза так как сперва он показывал ? на debounce, потом на ожидании собственно загрузки данных, а в конце уже показал загруженные данные.
При нажатии Reaload
опять же, не рендерится ничего лишнего — меняется лишь индикатор ожидания на кнопке, так как данные в итоге не поменялись:
render #counter-reload-safe
fetch GitHub.issue(48)
render #counter-reload-safe
render #counter-reload-safe
Ну а при быстром изменении номера — обновляется поле ввода текста и вывод зависящего от него заголовка:
render #counter-numb-input
render #counter-title-safe
render #counter-numb-input
render #counter-title-safe
fetch GitHub.issue(4)
render #counter-title-safe
render #counter-title-safe
Итого, какие проблемы мы решили:
✅ Компонент автоматически точечно (а не как с Redux) отслеживает внешние состояния.
✅ Параметры компонента обновляются без ререндера родителя.
❌ Перемещением компонент по прежнему управляет ReactJS.
✅ Изменение колбэка не приводит к ререндеру.
✅ Наш аналог хуков можно применять в любом месте кода, даже в циклах и условиях.
❌ Обработка ошибок по прежнему управляется ReactJS, поэтому требует ручной работы.
✅ Для частичного обновения можно создать компонент принимающий замыкание.
✅ stateful компоненты полснотью контролируемы.
Можете доработать этот пример и оформить в виде библиотеки типа remol, если готовы заниматься её поддержкой. Или реализовать подобную интеграцию для любого другого фреймворка. А мы пока отстыковываем первую ступень и летим ещё выше..
Реактивный JSX
Не сложно заметить, что отбирая у ReactJS контроль за состоянием, мы фактически низвергаем его с пьедестала фреймворка до уровня библиотеки рендеринга DOM, которой он изначально и являлся. Но это получается очень тяжёлая библиотека рендеринга, делающая слишком много лишней работы и тратящая впустую много памяти.
Давайте возьмём голый строго типизированный JSX, и сделаем его реактивным с помощью $mol_wire, получив полную замену ReactJS, но без VirtualDOM, но с точечными обновлениями реального DOM и другими приятными плюшками.
![](https://habrastorage.org/webt/yu/yb/l8/yuybl8xljnakvwhoneotiecqq2e.jpeg)
Для этого мы сперва возьмём $mol_jsx, который так же как E4X создаёт реальные DOM узлы, а не виртуальные:
const title = <h1 class="title" dataset={{ slug: 'hello' }}>{ this.title() }</h1>
const text = title.innerText // Hello, World!
const html = title.outerHTML // <h1 class="title" data-slug="hello">Hello, World!</h1>
Опа, нам больше не нужен ref для получения DOM узла из JSX, ведь мы сразу получаем от него DOM дерево.
Если исполнять JSX не просто так, а в контексте документа, то вместо создания новых элементов, будут использоваться уже существующие, на основе их идентификаторов:
<body>
<h1 id="title">...</h1>
</body>
$mol_jsx_attach( document, ()=> (
<h1 id="title" class="header">Wow!</h1>
) )
<body>
<h1 id="title" class="header">Wow!</h1>
</body>
Опа, мы получили ещё и гидратацию, но без разделения на первичный и вторичный рендеринг. Мы просто рендерим, а существующие элементы реиспользуются, если они есть.
Опа, да мы ж получили ещё и корректные перемещения компонентов, вместо их пересоздания в новом месте. Причём уже не в рамках одного родителя, а в рамках всего документа:
<body>
<article id="todo">
<h1 id="task/1">Complete article about $mol_wire</h1>
<article>
<article id="done"></article>
</body>
$mol_jsx_attach( document, ()=> (
<article id="done">
<h1 id="task/1">Complete article about $mol_wire</h1>
<article>
) )
<body>
<article id="todo"></article>
<article id="done">
<h1 id="task/1">Complete article about $mol_wire</h1>
<article>
</body>
Обратите внимание на использование естественных для HTML атрибутов id
и class
вместо эфемерных key
и className
.
В качестве тегов можно использовать, разумеется, и шаблоны (stateless функции), и компоненты (stateful классы). Первые просто вызываются с правильным контекстом, а значит безусловно рендерят своё содержимое. А вторые создают экземпляр объекта, делегируют ему управление рендерингом, и сохраняют ссылку на него в полученном DOM узле, чтобы использовать его снова при следующем рендеринге. В рантайме выглядит это как‑то так:
![](https://habrastorage.org/getpro/habr/upload_files/07b/954/193/07b9541939faba7d7af1e24369355fc3.png)
Тут мы видим два компонента, которые в результате рендеринга вернули один и тот же DOM элемент. Получить экземпляры компонент из DOM элемента не сложно:
const input = InputString.of( element )
Итак, давайте создадим простейший компонент - поле ввода текста:
export class InputString extends View {
// statefull!
@mem value( next = "" ) {
return next
}
// event handler
change( event: InputEvent ) {
this.value( ( event.target as HTMLInputElement ).value )
}
// apply state to DOM
render() {
return (
<input
value={ this.value() }
oninput={ action(this).change }
/>
)
}
}
Почти тот же код, что и с ReactJS, но:
Так как сверка при рендеринге происходит с реальным DOM, а не прошлой версией виртуального, то там не нужен костыль с немедленным обновлением виртуального DOM после обработки события, чтобы при вводе каретка не улетала в конец.
События приходят нативные, а не синтетические, что избавляет от кучи неожиданностей.
Классы для стилизации генерируются автоматически на основе идентификаторов и имён компонент.
Нет необходимости руками собирать идентификаторы элементов — семантичные идентификаторы тоже формируются автоматически.
Для корневого элемента идентификатор вообще не нужно указывать — он устанавливается равным идентификатору компонента.
При конфликте идентификаторов кидается исключение, что гарантирует их глобальную уникальность.
Для иллюстрации последних пунктов, давайте рассмотрим более сложный компонент — поле ввода числа:
export class InputNumber extends View {
// self state
@mem numb( next = 0 ) {
return next
}
dec() {
this.numb( this.numb() - 1 )
}
inc() {
this.numb( this.numb() + 1 )
}
// lifted string state as delegate to number state!
@mem str(str?: string) {
const next = str?.valueOf && Number( str )
if( Object.is( next, NaN ) ) return str ?? ""
const res = this.numb(next)
if( next === res ) return str ?? String( res ?? "" )
return String( res ?? "" )
}
render() {
return (
<div>
<Button
id="decrease"
action={ () => this.dec() }
title={ () => "➖" }
/>
<InputString
id="input"
value={ next => this.str( next ) } // hack to lift state up
/>
<Button
id="increase"
action={ () => this.inc() }
title={ () => "➕" }
/>
</div>
)
}
}
По сгенерированным классам легко навешивать стили на любые элементы:
/** bem-block */
.InputNumber {
border-radius: 0.25rem;
box-shadow: 0 0 0 1px gray;
display: flex;
overflow: hidden;
}
/** bem-element */
.InputNumber_input {
flex: 1 0 auto;
}
/** bem-element of bem-element */
.Counter_numb_input {
color: red;
}
К сожалению, реализовать полноценный CSS‑in‑TS в JSX не представляется возможным, но даже только лишь автогенерация классов уже существенно упрощает стилизацию.
Чтобы всё это работало, надо реализовать лишь базовый класс для реактивных JSX компонент:
/** Reactive JSX component */
abstract class View extends $mol_object2 {
/** Returns component instance for DOM node. */
static of< This extends typeof $mol_jsx_view >( this: This, node: Element ) {
return node[ this as any ] as InstanceType< This >
}
// Allow overriding of all fields via attributes
attributes!: Partial< Pick< this, Exclude< keyof this, 'valueOf' > > >
/** Document to reuse DOM elements by ID */
ownerDocument!: typeof $mol_jsx_document
/** Autogenerated class names */
className = ''
/** Children to render inside */
@ $mol_wire_field
get childNodes() {
return [] as Array< Node | string >
}
/** Memoized render in right context */
@ $mol_wire_solo
valueOf() {
const prefix = $mol_jsx_prefix
const booked = $mol_jsx_booked
const crumbs = $mol_jsx_crumbs
const document = $mol_jsx_document
try {
$mol_jsx_prefix = this[ Symbol.toStringTag ]
$mol_jsx_booked = new Set
$mol_jsx_crumbs = this.className
$mol_jsx_document = this.ownerDocument
return this.render()
} finally {
$mol_jsx_prefix = prefix
$mol_jsx_booked = booked
$mol_jsx_crumbs = crumbs
$mol_jsx_document = document
}
}
/** Returns actual DOM tree */
abstract render(): HTMLElement
}
Наконец, закончив с приготовлениями, напишем уже наше приложение:
export class Counter extends View {
@mem numb( value = 48 ) {
return value
}
issue( reload?: "reload" ) {
return GitHub.issue( this.numb(), reload )
}
title() {
return this.issue().title
}
link() {
return this.issue().html_url
}
render() {
return (
<div>
<InputNumber
id="numb"
numb={ next => this.numb(next) } // hack to lift state up
/>
<Safe
id="titleSafe"
task={ ()=> (
<a id="title" href={ this.link() }>
{ this.title() }
</a>
) }
/>
<Button
id="reload"
action={ ()=> this.issue("reload") }
title={ ()=> "Reload" }
/>
</div>
)
}
}
Весь код этого примера можно найти в песочнице. Вот так вот за 1 вечер мы реализовали свой ReactJS на $mol, добавив кучу уникальных фичей, но уменьшив объём бандла в 5 раз. По скорости же мы идём ноздря в ноздрю с оригиналом:
![](https://habrastorage.org/getpro/habr/upload_files/185/bc4/767/185bc4767b3dd73898d845e1046c7408.png)
А как насчёт обратной задачи — написать аналог фреймворка $mol на ReactJS? Вам потребуется минимум 3 миллиона долларов, команда из десятка человек и несколько лет ожидания. Но мы не будем ждать, а отстыкуем и эту ступень..
Реактивный DOM
Раньше DOM был медленным и не удобным. Чтобы с этим совладать были придуманы разные шаблонизаторы и техники VirtualDOM, IncrementalDOM, ShadowDOM. Однако, фундаментальные проблемы RealDOM никуда не деваются:
1. Жадность. Браузер не может в любое время спросить прикладной код «хочу отрендерить эту часть страницы, сгенерируй мне элементов с середины пятого до конца седьмого». Нам приходится сначала сгенерировать огромный DOM, чтобы браузер показал лишь малую его часть. А это крайне ресурсоёмко.
2. Безучастность. Состояние DOM логически зависит как от прикладных состояний, так и от состояний самого DOM. Но браузер не понимает этих зависимостей, не может их гарантировать, и не может оптимизировать обновление DOM.
3. Тернистость. На самом деле DOM нам и не нужен. Нам нужен способ сказать браузеру как и когда рендерить наши компоненты.
Ну да ладно, давайте представим, что было бы, если бы DOM и весь остальной рантайм были реактивными. Мы могли бы безо всяких библиотек связать любые состояния через простые инварианты и браузер бы гарантировал их выполнения максимально оптимальным способом!
![](https://habrastorage.org/webt/zs/ji/wz/zsjiwz4h4mjdpza8il-cnh1qxx8.jpeg)
Я набросал небольшой пропозал, как это могло бы выглядеть. Для примера, давайте возьмём и привяжем текст параграфа к значению поля ввода:
<input id="input" />
<p id="output"></p>
<script>
const input = document.getElementById('input')
const output = document.getElementById('output')
Object.defineProperty( output, 'innerText', {
get: ()=> 'Hello ' + input.value
} )
</script>
И всё, никаких библиотек, никаких обработчиков событий, никаких DOM-манипуляций. Только наши желания в чистом виде.
А хотите попробовать ReactiveDOM в деле уже сейчас? Я опубликовал прототип полифила $mol_wire_dom. Он не очень эффективен, много чего не поддерживает, но для демонстрации сойдёт:
<div id="root">
<div id="form">
<input id="nickname" value="Jin" />
<button id="clear">Clear</button>
<label>
<input id="greet" type="checkbox" /> Greet
</label>
</div>
<p id="greeting">...</p>
</div>
import { $mol_wire_dom, $mol_wire_patch } from "mol_wire_dom";
// Make DOM reactive
$mol_wire_dom(document.body);
// Make globals reactive
$mol_wire_patch(globalThis);
// Take references to elements
const root = document.getElementById("root") as HTMLDivElement;
const form = document.getElementById("form") as HTMLDivElement;
const nickname = document.getElementById("nickname") as HTMLInputElement;
const greet = document.getElementById("greet") as HTMLInputElement;
const greeting = document.getElementById("greeting") as HTMLParagraphElement;
const clear = document.getElementById("clear") as HTMLButtonElement;
// Setup invariants
Object.assign(root, {
childNodes: () => (greet.checked ? [form, greeting] : [form]),
style: () => ({
zoom: 1 / devicePixelRatio
})
});
Object.assign(greeting, {
textContent: () => `Hello ${nickname.value}!`
});
// Set up handlers
clear.onclick = () => (nickname.value = "");
Тут мы применили ещё и $mol_wire_patch
чтобы сделать глобальные свойства реактивными. Поэтому при изменении зума браузера размер интерфейса будет меняться так, чтобы это компенсировать. При нажатии на кнопку введённое в поле имя будет очищаться. А отображаться текущее имя будет в приветствии, которое показывается только, когда чекбокс взведён.
Ленивый DOM
А теперь представьте, как было бы классно, если бы браузеры поддержали всё это, да без полифилов. Мы могли бы писать легко поддерживаемые веб приложения даже без фреймворков. А с фреймворком могло бы быть и ещё лаконичней, но всё ещё легковесно.
![](https://habrastorage.org/webt/fj/vt/iw/fjvtiwjq-txjlaa_t_qm7ndjwio.jpeg)
Вы только гляньте, как фреймворк, построенный на $mol_wire, просто уничтожает как низкоуровневых конкурентов, так даже и VanillaJS:
![](https://habrastorage.org/getpro/habr/upload_files/643/cac/fa4/643cacfa45242ccb2b4b1f87ed2cec4c.png)
И дело тут не в том, что он так быстро рендерит DOM, а как раз наоборот, в том, что он не рендерит DOM, когда он вне видимой области, даже если это сложная вёрстка, а не плоский список с фиксированной высотой строк.
А представьте как ускорился бы web, если сами браузеры научились бы так делать — запрашивать у прикладного кода ровно то, что необходимо для отображения, и самостоятельно следить за зависимостями.
Когда я показываю подобные картинки, меня часто обвиняют в нечестности, ведь к другим фреймворкам тоже можно прикрутить virtual‑scroll и будет быстро. Или предлагают отключить виртуальный рендеринг, чтобы уравнять реализации по самому низкому уровню. Это всё равно что делать лоботомию Каспарову для уравнения шансов, так как он слишком хорошо играет в шахматы.
Однако, важно понимать разницу между поведением по умолчанию и поведением, требующим долгой и аккуратной реализации, да ещё и с кучей ограничений:
![](https://habrastorage.org/getpro/habr/upload_files/a93/25e/152/a9325e152e0d00bc8c3f6a6f16bf5462.png)
Именно поэтому вы почти не встретите виртуального рендеринга в приложениях на других фреймворках. И именно поэтому вы почти не встретите приложений без виртуализации на $mol.
Грамотная реализация виртуального рендеринга — не самая простая задача, особенно учитывая не оптимизированную для этого архитектуру большинства фреймворков. Я подробно рассказывал об этом в докладе:
На мой взгляд только LazyDOM может обеспечить нас отзывчивыми интерфейсами во всё более раздувающихся объёмах данных и во всё более снижающемся уровне подготовки прикладных разработчиков. Потому нам нужно продавить его внедрение в браузеры.
Но, как показывает мой опыт, пропозалы писать бесполезно — их просто игнорируют. Нужно взять на вооружение тактику обещаний: сначала множество библиотек начали их использовать, а потом браузеры втянули их в себя и стандартизовали.
Вот и тут нам, разработчикам, нужно уже начинать внедрять поддержку этого реактивного клея, чтобы различные библиотеки могли хорошо дружить друг с другом, встраиваясь в единую реактивную систему, а не требовать от прикладных программистов постоянного ручного перекладывания данных между разнородными хранилищами.
Если вы разрабатываете библиотеку или фреймворк, и мне удалось убедить вас поддержать общий реактивный API, то свяжитесь со мной, чтобы мы обсудили детали. Интеграция возможна как на уровне интерфейсов путём реализации полностью своих подписчиков и издателей, так и можно взять готовые части $mol_wire, чтобы не париться с велосипедами.
Фреймворк на основе $mol_wire
Наконец, позвольте показать вам, как тот же продвинутый счётчик реализуется на $mol, который я всю статью тизерил..
Для загрузки данных есть стандартный модуль ($mol_fetch
). Более того, для работы с GitHub есть стандартный модуль ($mol_github
). Так же возьмём стандартные кнопки ($mol_button
), стандартные ссылки ($mol_link
), стандартные поля ввода текста ($mol_string
) и числа ($mol_number
), завернём всё в вертикальный список ($mol_list
) и вуаля:
$my_counter $mol_list
Issue $mol_github_issue
title => title
web_uri => link
json? => data?
sub /
<= Numb $mol_number
value? <=> numb? 48
<= Title $mol_link
title <= title
uri <= link
<= Reload $mol_button_minor
title @ \Reload
click? <=> reload?
export class $my_counter extends $.$my_counter {
Issue() {
const endpoint = `https://api.github.com/repos`
const uri = `${ endpoint }/nin-jin/HabHub/issues/${ this.numb() }`
return this.$.$mol_github_issue.item( uri )
}
reload() {
this.data( null )
}
}
![](https://habrastorage.org/getpro/habr/upload_files/b98/305/9c0/b983059c05144d041587c54b7591d555.png)
При даже чуть большей функциональности (например, поддержка цветовых тем, локализации и пр), кода на $mol получилось в 2 раза меньше, чем в варианте с JSX. А главное — уменьшилась когнитивная сложность. Но это уже совсем другая история..
Пока же, приглашаю вас попробовать $mol_wire в своих проектах. А если у вас возникнут сложности, не стесняйтесь задавать вопросы в теме про $mol на форуме Hyper Dev.
![](https://habrastorage.org/getpro/habr/upload_files/948/8f9/8c9/9488f98c90c6017e7f4ec07e40b29026.png)