Трансплантация реактивности

от автора

Здравствуйте, меня зовут Дмитрий Карловский, и я.. тот самый чел, который написал реактивную библиотеку $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.


Актуальный оригинал на $hyoo_page


ссылка на оригинал статьи https://habr.com/ru/post/723728/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *