Здравствуйте, меня зовут Дмитрий Карловский, и я.. тот самый чел, который написал реактивную библиотеку $mol_wire. Именно благодаря мне вам есть сейчас чем пугать детей перед сном.
Но просто написать классную библиотеку - слишком мелкая цель. Построить на ней богатый фреймворк с кучей батареек - уже интересней, но всё ещё не достаточно амбициозно. Разработанный мной подход может стать lingua franca в коммуникациях между библиотеками, состояниями браузера, и даже между удалёнными узлами.
Берегите синапсы, сейчас будет настоящий киберпанк..
Реактивный React
ReactJS сейчас самый популярный фреймворк, вопреки множеству архитектурных просчётов. Вот лишь некоторые из них:
Компонент не отслеживает внешние состояния, от которых он зависит, — обновляется он только при изменении локального. Это требует аккуратных подписок/отписок и своевременных уведомлений об изменениях.
Единственный способ изменить один параметр компонента — это полностью ререндерить внешний компонент, заново сформировав все параметры как для него, так и для соседей. То же касается и добавления/удаления/перемещения компонента.
При перемещении компонента между контейнерами происходит его полное пересоздание. И наоборот, разные экземпляры одного компонента могут быть неуместно реиспользованы.
Создаваемые на лету колбэки приводят к лишним ререндерам, поэтому требуют аккуратной их мемоизации с точным указанием используемых внутри них переменных.
Хуки нельзя применять в условиях, циклах и других местах динамичности потока исполнения, иначе всё сломается.
Ошибки и индикация ожидания происходят вне компонента. Компонент получается не самодостаточным и фатально влияющим и на внешний компонент и, как следствие, на соседей.
Компонент невозможно обновить частично — только полный ререндер. Чтобы это побороть либо обмазываются мемоизацией, либо излишне увеличивают гранулярность компонент.
Отсутствие контроля stateful компонента часто приводит к необходимости разбивать каждый компонент на два: контролируемый stateless и неконтролируемая stateful обёртка над ним. Частичный контроль при этом сопряжён с трудностями и копипастой.
Что ж, давайте вылечим больного, а заодно покажем простоту интеграции реактивной библиотеки $mol_wire в совершенно инородную ему архитектуру.
Начнём издалека — напишем синхронную функцию, которая загружает 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
.
Весь код этого примера можно найти в песочнице:
Там добавлены ещё и логи, чтобы можно было понять что происходит. Например, вот так выглядит первичный рендеринг:
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 и другими приятными плюшками.
Для этого мы сперва возьмём $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 узле, чтобы использовать его снова при следующем рендеринге. В рантайме выглядит это как‑то так:
Тут мы видим два компонента, которые в результате рендеринга вернули один и тот же 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 раз. По скорости же мы идём ноздря в ноздрю с оригиналом:
А как насчёт обратной задачи — написать аналог фреймворка $mol на ReactJS? Вам потребуется минимум 3 миллиона долларов, команда из десятка человек и несколько лет ожидания. Но мы не будем ждать, а отстыкуем и эту ступень..
Реактивный DOM
Раньше DOM был медленным и не удобным. Чтобы с этим совладать были придуманы разные шаблонизаторы и техники VirtualDOM, IncrementalDOM, ShadowDOM. Однако, фундаментальные проблемы RealDOM никуда не деваются:
1. Жадность. Браузер не может в любое время спросить прикладной код «хочу отрендерить эту часть страницы, сгенерируй мне элементов с середины пятого до конца седьмого». Нам приходится сначала сгенерировать огромный DOM, чтобы браузер показал лишь малую его часть. А это крайне ресурсоёмко.
2. Безучастность. Состояние DOM логически зависит как от прикладных состояний, так и от состояний самого DOM. Но браузер не понимает этих зависимостей, не может их гарантировать, и не может оптимизировать обновление DOM.
3. Тернистость. На самом деле DOM нам и не нужен. Нам нужен способ сказать браузеру как и когда рендерить наши компоненты.
Ну да ладно, давайте представим, что было бы, если бы DOM и весь остальной рантайм были реактивными. Мы могли бы безо всяких библиотек связать любые состояния через простые инварианты и браузер бы гарантировал их выполнения максимально оптимальным способом!
Я набросал небольшой пропозал, как это могло бы выглядеть. Для примера, давайте возьмём и привяжем текст параграфа к значению поля ввода:
<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
А теперь представьте, как было бы классно, если бы браузеры поддержали всё это, да без полифилов. Мы могли бы писать легко поддерживаемые веб приложения даже без фреймворков. А с фреймворком могло бы быть и ещё лаконичней, но всё ещё легковесно.
Вы только гляньте, как фреймворк, построенный на $mol_wire, просто уничтожает как низкоуровневых конкурентов, так даже и VanillaJS:
И дело тут не в том, что он так быстро рендерит DOM, а как раз наоборот, в том, что он не рендерит DOM, когда он вне видимой области, даже если это сложная вёрстка, а не плоский список с фиксированной высотой строк.
А представьте как ускорился бы web, если сами браузеры научились бы так делать — запрашивать у прикладного кода ровно то, что необходимо для отображения, и самостоятельно следить за зависимостями.
Когда я показываю подобные картинки, меня часто обвиняют в нечестности, ведь к другим фреймворкам тоже можно прикрутить virtual‑scroll и будет быстро. Или предлагают отключить виртуальный рендеринг, чтобы уравнять реализации по самому низкому уровню. Это всё равно что делать лоботомию Каспарову для уравнения шансов, так как он слишком хорошо играет в шахматы.
Однако, важно понимать разницу между поведением по умолчанию и поведением, требующим долгой и аккуратной реализации, да ещё и с кучей ограничений:
Именно поэтому вы почти не встретите виртуального рендеринга в приложениях на других фреймворках. И именно поэтому вы почти не встретите приложений без виртуализации на $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 )
}
}
При даже чуть большей функциональности (например, поддержка цветовых тем, локализации и пр), кода на $mol получилось в 2 раза меньше, чем в варианте с JSX. А главное — уменьшилась когнитивная сложность. Но это уже совсем другая история..
Пока же, приглашаю вас попробовать $mol_wire в своих проектах. А если у вас возникнут сложности, не стесняйтесь задавать вопросы в теме про $mol на форуме Hyper Dev.
Комментарии (31)
VladimirFarshatov
00.00.0000 00:00+1Не увидел в табличке сравнения как с чистым JS, так и с низкоуровневой (будем считать так) библиотекой JQuery. Возможно такой пример был бы и сложнее и даже существенно, но в качестве сравнительной колонки, кмк, просто обязан присутствовать. А так .. в JS можно писать любые "обфускаторы" и надстройки.. дорого всё это, что наглядно видно на страницах сайтов, как только спускаешься с небес на землю. Достаточно попасть в какой-то уголок, где штатная скорость 20-50кБод и фсё .. можно покурить, налить и выпить кофе пока прогрузятся страницы с очередным модным шаблонизатором - улучшателем ДОМ .. :(
В общем, табличку стоит таки дополнить.
Ahuromazdie
00.00.0000 00:00-2А ваниллажс это разве не ванильный жс? Ну т.е. " Vanilla (ПО) — оригинальная немодифицированная версия программного обеспечения."
VladimirFarshatov
00.00.0000 00:00+1Думаю там про этот фреймворк http://vanilla-js.com/
Иначе как-то слабо верится что обфускатор, генерирующий в конечном итоге JS-код, работает быстрее оригинального js. "так не бывает".. ;)
nin-jin Автор
00.00.0000 00:00Бандл всего приведённого в статье приложения на $mol весит меньше одной только голой библиотеки jQuery без плагинов, стилей и прикладного кода.
VladimirFarshatov
00.00.0000 00:00+1Ну не фронтендщик, бэк в основном. На своих хромоногих фронтах дальше JQuery не видел надобности применять что-либо. Из фронтовой работы, имею опыт по выпиливанию Ангуляра с заменой на чистый JS. По результатам этой работы могу сказать что страница сайта полегчала примерно с 2 Мб, до 150кб и ускорилась от 20 до 200 раз.
Да, тоже "толстый клиент", запрашивающий обновления с сайта и с частичным обновлением ДОМ по мере работы. Сметная программа, считающая смету на клиенте, запрашивающая данные по товарам, работам, ценам и отправляющая окончательную смету на сервер..
bromzh
00.00.0000 00:00Как я уже писал - не взлетит, как минимум, потому что snake_case. Лапшу из разного формата кодирования никто в здравом уме не хочет видеть у себя на проектах. И дело даже не в том, что хуже или лучше, это дело вкуса. Если вы не уважаете сообщество, используя не то форматирование, то зачем сообществу в ответ пытаться использовать ваше творение?
nin-jin Автор
00.00.0000 00:00А заслуживает ли уважения сообщество, которое добровольно отказывается от повышения эффективности своей работы, оправдывая это собственными бестолковыми привычками, не способное принимать рациональные решения, эгоцентрично считающее, что своим потребительским отношением, оказывают мне некую услугу, ради которой я буду за ним бегать и уговаривать взяться за ум?
yroman
00.00.0000 00:00+2>> ради которой я буду за ним бегать и уговаривать взяться за ум
Если сообщество не заслуживает вашего уважения, то зачем вы за ним бегаете тогда? Вы ведь на хабре делаете именно это, уже довольно долгое время популяризируя свой фреймворк. Что это, как не беготня за сообществом и уговаривание его взяться за ум?VladimirFarshatov
00.00.0000 00:00+6Сообщество хабра, кмк, разнородно как по направлениям разработки, так и по бэк-фронт, по применяемым языкам и тем более сторонним библиотекам. Если автор пиарит некое свое решение, то это может и вовсе не означать "пиар", как его продвижение, а, к примеру, демонстрация "во как я могу", или "есть и такое решение" среди множества иных. Но! Даже если это и пиар, поскольку автору решение кажется "самым клевым", то это не значит что оно продвигается на всё сообщество целиком. Тут, кмк, действует "свобода выбора": не нравится - не читай, делов-то..
bromzh
00.00.0000 00:00-1Ваши якобы рациональные решения по смешению стиля кода ломаются о первый же линтер, который будет у каждой нормальной команды.
nin-jin Автор
00.00.0000 00:00Рационально как раз не смешивать стили. А от линтера больше вреда, чем пользы.
bromzh
00.00.0000 00:00+1Тогда странно, что ваш рациональный подход генерирует нерациональный, с ваших же слов, код.
А от линтера больше вреда, чем пользы.
От подсветки синтаксиса, кстати, тоже. Роб Пайк подтвердит.
В целом, линтер вреден, да. Например, внедрение sort-imports на всех моих проектах уменьшило количество merge-conflicts радикально. И после этого, вместо просиживания штанов за разрешением конфликтов приходилось код писать, вот отстой.
nin-jin Автор
00.00.0000 00:00-1Очевидно потому, что я отвечаю лишь за свой код, но не сторонние апи, которые могут быть любой кривизны. Яркие примеры:
XMLHttpRequest
,onsecuritypolicyviolation
.
jodaka
00.00.0000 00:00+2Круто! На самом деле круто.
(вот уж не думал, что напишу такое под статьёй Карловского)
bO_oblik
00.00.0000 00:00+1Спасибо! У вас очень интересный контент, я требую продолжить гнуть свою линию и не оглядываться на "унылых" ребят.
flancer
00.00.0000 00:00+1Интересно, чем $mall так плох, что требует настолько агрессивный маркетинг? Ведь это же просто "реактивная библиотека", почему именно ей "пугают детей перед сном"? Если эта библиотека настолько хороша (да даже не настолько, а просто хороша), то почему бы на ней не делать проекты и не дать ей жить своей жизнью? Прорастёт со временем. Если, конечно, у неё есть реальные преимущества перед другими. Вон, @VladimirFarshatov чуть выше заменил в каком-то проекте ангуляр на чистый JS и получил выигрыш на порядки (!). Может вместо статей на хабре заменять что-угодно на $small в тех проектах, где $mall может показать себя с наилучшей стороны? А уже потом писать статьи на хабре? Со ссылкой на эти проекты и на $small. Так-то оно не настолько пугающе будет.
P.S.
Ссылки на проекты, построенные на $small изначально - то такое себе, малоубедительное. Для соответствующего впечатления нужно, как у @VladimirFarshatov: было то-то, поставили $small - стало в XX раз быстрее. Только с цифрами до и после. И со ссылками на владельцев проекта. Тогда уже бизнес будет заинтересован в этой технологии. Если у неё действительно есть преимущества.
nin-jin Автор
00.00.0000 00:00Вы тоже статью до конца не дочитали? Там есть яркий пример до и после.
flancer
00.00.0000 00:00+1Да, я тоже до конца статью не дочитал. Если вы хотите, чтобы у меня в голове ярко отложилось до и после, то разжуйте, положите мне в рот и спросите: "Ну как? Здорово ведь, да? Может ещё?" Я спецом сбегал ещё раз до конца статьи и не увидел ничего такого, ради чего стоит опять вчитываться в буквы, которые там написаны. Или вы всерьёз считаете, что вот этот виджет - это проект, который может реально заинтриговать бизнес?
Боюсь, вы просто не понимаете, что я имел в виду под словом "проект" в своём комменте выше. Но я и не заинтересован в том, чтобы вы это понимали. Я использую Vue у себя. Я инвестировал своё время в изучение этой технологии и мне не выгодно, чтобы её вытеснила какая-либо другая.
nin-jin Автор
00.00.0000 00:00-2Если вам нужен менторинг, то можете заказать у меня эту услугу. Но сразу предупреждаю, это не дешёвое удовольствие, и я буду заставлять вас таки вчитываться в статьи, которые вы берётесь о
бсуждать.flancer
00.00.0000 00:00+1А что вы можете мне дать? У меня full stack - file system, RDBMS, SSE, Web Sockets, IndexedDB, Service Workers, интеграция с внешними API как с фронта, так и с бэка (STT, OCR). Ваша реактивность лишь малая часть общего функционала в проектах, с которыми я имею дело. Мне не нужна "глубина погружения", мне нужно оставаться в текущем тренде, "на гребне волны". А для этого пока что хватает Vue. Если куда и переключаться, то на Svelte:
nin-jin Автор
00.00.0000 00:00-3Для начала я могу дать вам почитать свои статьи, но не забесплатно, конечно.
flancer
00.00.0000 00:00Для чего? Для чего я буду читать ваши статьи? Чтобы что? Ознакомиться с ещё одной технологией/библиотекой/платформой? Я знаю, для чего нужен
./autoexec.bat
в MS-DOS. Иconfig.sys
тоже знаю, для чего. Я знаю, как устанавливать и конфигурить OS/2 Warp и Solaris. Но кто бы мне объяснил, зачем мне сейчас все эти знания? У меня в голове полно ненужных знаний. Зачем мне ещё одно. К тому же незабесплатно.Я лучше забесплатно понаблюдаю за тем, как не надо пиарить свой продукт. У вас это хорошо получается. Извините, что пытался помешать :)
nin-jin Автор
00.00.0000 00:00-4Какой-то вы не современный, вон, уже даже ChatGPT научился в $mol, а вы до сих пор мусор про DOS на антресолях храните.
ionicman
Очередной 100500 фреймворк, который "лучше всех" и пытается приделать к Реакту то, что бай дизайн ему не свойственно - стандартный реакт и ведет себя по стандартному - любой человек, его знающий понимает слабые и сильные его стороны и разберется с проектом. А тут будут проклятья и маты - пусть даже фреймворк и сделает что-то лучше.
При этом это очередной фреймворк без коммьюнити, разрабатываемый одним человеком, который считает, что сделал серебраянную пулю (нет).
За притаскивание такого в проект я бы лично обрывал руки - почему написал выше.
Все это, естественно, не умаляет заслуг автора по созданию данного фреймворка, но его агрессивный пиар в купе с вечным поливанием гном других фреймворков достал, честно.
Естественно это мое и только мое мнение - ничего никому не навязываю.
Ahuromazdie
Слышал звон да не знаю где он? $mol ничего к Реакту не приделывает. $mol модульный, и его модули можно использовать даже с Реактом...
nin-jin Автор
Не стоит преувеличивать, комьюнити хоть и небольшое, но есть, и активно участвует в улучшении фреймворка. Ну а про поливание гном, отвечу словами ВВП: "Я ещё даже не начинал".