Здравствуйте, меня зовут Дмитрий Карловский и я… крайне плох в построение социальных связей, но чуть менее плох в построении программных. Недавно я подытожил свой восьмилетний опыт реактивного программирования, проведя обстоятельный анализ различных подходов к решению типичных детских болячек:
Main Aspects of Reactivity
Я очень рекомендую прочитать сперва ту статью, чтобы лучше понимать дальнейшее повествование, где мы с нуля разработаем совершенно новую TypeScript реализацию, вобравшую в себя все самые крутые идеи, позволяющие достигнуть беспрецедентной выразительности, компактности, скорости, надёжности, простоты, гибкости, экономности..

Статья разбита на главы, перелинкованные с соответствующими аспектами из упомянутого выше анализа. Так что если вдруг потеряетесь — сможете быстро восстановить контекст.
Повествование будет долгим, но если вы продержитесь до конца, то сможете смело идти к начальнику за повышением. Даже если вы сам себе начальник.
К сожалению, статья получилась слишком длинной для Хабра, поэтому полную версию вы найдёте среди остальных статей. Далее же я подготовил для вас краткое оглавление, чтобы вы понимали сколько всего вас там ждёт.
- Рассматриваются разные абстракции работы состоянием: поля, хуки, и вводится новый тип — каналы, позволяющие через одну функцию и проталкивать значения и затягивать, полностью контролируя оба процесса.
- Приводятся примеры работы через канал с локальной переменной, обработки событий, делегирование работы другому каналу с образованием цепочек, пересекающих разные слои абстракции.
let _title = ''
const title = ( text = _title )=> _title = text
title() // ''
title( 'Buy some milk' ) // 'Buy some milk'
title() // 'Buy some milk'
- Рассматривается использование каналов в качестве методов объектов.
- Вводится декоратор
$mol_wire_solo
, мемоизирующий их работу для экономии вычислений и обеспечения идемпотентности.
class Task extends Object {
@ $mol_wire_solo
title( title = '' ) {
return title
}
details( details?: string ) {
return this.title( details )
}
}
- Расcматривается композиция нескольких простых каналов в один составной.
- И наоборот — работа с составным каналом через несколько простых.
class Task extends Object {
@ $mol_wire_solo
title( title = '' ) { return title }
@ $mol_wire_solo
duration( dur = 0 ) { return dur }
@ $mol_wire_solo
data( data?: {
readonly title?: string
readonly dur?: number
} ) {
return {
title: this.title( data?.title ),
dur: this.duration( data?.dur ),
} as const
}
}
- Рассматриваются каналы, мультиплексированные в одном методе, принимающем первым аргументом идентификатор канала.
- Вводится новый декоратор
$mol_wire_plex
для таких каналов.
- Демонстрируется подход с выносом копипасты из нескольких сольных каналов в один мультиплексированный в базовом классе без изменения API.
- Демонстрируется вынос хранения состояний множества объектов в локальное хранилище через мультиплексированный синглтон с получением автоматической синхронизации вкладок.
class Task_persist extends Task {
@ $mol_wire_solo
data( data?: {
readonly title: string
readonly dur: number
} ) {
return $mol_state_local.value( `task=${ this.id() }`, data )
?? { title: '', cost: 0, dur: 0 }
}
}
// At first tab
const task = new Task_persist( 777 )
task.title( 'Buy some milk' ) // 'Buy some milk'
// At second tab
const task = new Task_persist( 777 )
task.title() // 'Buy some milk'
- Реализуется библиотека выдающая уникальный строковый ключ для эквивалентных сложных структур.
- Объясняется универсальный принцип поддержки и пользовательских типов данных.
- Демонстрируется её применение для идентификации мультиплексированных каналов.
@ $mol_wire_plex
task_search( params: {
query?: string
author?: Person[],
assignee?: Person[],
created?: { from?: Date, to?: Date }
updated?: { from?: Date, to?: Date }
order?: { field: string, asc: boolean }[]
} ) {
return this.api().search( 'task', params )
}
- Вводится понятие реактивного фабричного метода, управляющего жизненным циклом создаваемого объекта.
- Рассматривается ленивое создание цепочки объектов с последующим автоматическим её разрушением.
- Объясняется принцип захвата владения объектом и предсказуемость момента его разрушения.
- Подчёркивается важность ленивого создания объектов для скорости компонентного тестирования.
class Account extends Entity {
@ $mol_wire_plex
project( id: number ) {
return new Project( id )
}
}
class User extends Entity {
@ $mol_wire_solo
account() {
return new Account
}
}
- Рассматривается техника настройки объекта путём переопределения его каналов.
- Демонстрируется поднятие стейта используя хакинг.
- Подчёркиваются преимущества хакинга для связывания объектов ничего не знающих друг про друга.

- Связывание объектов классифицируются по направлению: одностороннее и двустороннее.
- А так же по методу: делегирование и хакинг.
- Подчёркивается недостатки связывания методом синхронизации.
class Project extends Object {
@ $mol_wire_plex
task( id: number ) {
const task = new Task( id )
// Hacking one-way
// duration <= task_duration*
task.duration = ()=> this.task_duration( id )
// Hacking two-way
// cost <=> task_cost*
task.cost = next => this.task_cost( id, next )
return task
}
// Delegation one-way
// status => task_status*
task_status( id: number ) {
return this.task( id ).status()
}
// Delegation two-way
// title = task_title*
task_title( id: number, next?: string ) {
return this.task( id ).title( next )
}
}
- Раскрываются возможности фабрик по формированию глобально уникальных семантичных идентификаторов объектов.
- Демонстрируется отображение индентификаторов в отладчике и стектрейсах.
- Демонстрируется использование custom formatters для ещё большей информативности объектов в отладчике.
- Демонстрируется логирование изменений состояний с отображением их идентификаторов.

- Вводится понятие волокна — приостанавливаемой функции.
- Оценивается потребление памяти наивной реализацией волокна на хеш-таблицах.
- Предлагается наиболее экономная реализация на обычном массиве.
- Раскрывается техника двусторонних связей с накладными расходами всего в 16 байт и константной алгоритмической сложностью операций.
- Обосновывается ограниченность разрастания занимаемой массивом памяти при динамической перестройке графа.

- Вводится понятия издателя, как минимального наблюдаемого объекта.
- Оценивается потребление памяти издателем.
- Демонстрируется применение издателя для реактивизации обычной переменной и адреса страницы.
- Предлагается к использованию микро библиотека, предоставляющая минимального издателя для встраивания в другие библиотеки.
- Демонстрируется создание реактивного множества из нативного.
const pub = new $mol_wire_pub
window.addEventListener( 'popstate', ()=> pub.emit() )
window.addEventListener( 'hashchange', ()=> pub.emit() )
const href = ( next?: string )=> {
if( next === undefined ) {
pub.promote()
} else if( document.location.href !== next ) {
document.location.href = next
pub.emit()
}
return document.location.href
}
- Разбирается структурное сравнение произвольных объектов.
- Вводится эвристика для поддержки пользовательских типов данных.
- Обосновывается важность кеширования и разъясняется как избежать утечек памяти при этом.
- Раскрывается применение кеширования для корректного сравнения циклических ссылок.
- Предлагается к использованию независимая микро библиотека.
- Приводятся результаты сравнения производительности разных библиотек глубокого сравнения объектов.

- Вводится понятие подписчика, как наблюдателя способного автоматически подписываться на издателей и отписываться от них.
- Оценивается потребление памяти подписчиком и подписчиком совмещённым с издателем.
- Раскрывается алгоритм автоматической подписки на издателей.
- Разбирается ручная низкоуровневая работа с подписчиком.
const susi = new $mol_wire_pub_sub
const pepe = new $mol_wire_pub
const lola = new $mol_wire_pub
const backup = susi.track_on() // Begin auto wire
try {
touch() // Auto subscribe Susi to Pepe and sometimes to Lola
} finally {
susi.track_cut() // Unsubscribe Susi from unpromoted pubs
susi.track_off( backup ) // Stop auto wire
}
function touch() {
// Dynamic subscriber
if( Math.random() < .5 ) lola.promote()
// Static subscriber
pepe.promote()
}
- Вводится понятие задачи, как одноразового волокна, которое финализируется при завершении, освобождая ресурсы.
- Сравниваются основные виды задач: от нативных генераторов и асинхронных функций, до NodeJS расширения и SuspenseAPI с перезапусками функции.
- Вводится декоратор
$mol_wire_task
автоматически заворачивающий метод в задачу.
- Разъясняется как бороться с неидемпотентностью при использовании задач.
- Раскрывается механизм обеспечения надёжности при перезапусках функции с динамически меняющимся потоком исполнения.
// Auto wrap method call to task
@ $mol_wire_method
main() {
// Convert async api to sync
const syncFetch = $mol_wire_sync( fetch )
this.log( 'Request' ) // 3 calls, 1 log
const response = syncFetch( 'https://example.org' ) // Sync but non-blocking
// Synchronize response too
const syncResponse = $mol_wire_sync( response )
this.log( 'Parse' ) // 2 calls, 1 log
const response = syncResponse.json() // Sync but non-blocking
this.log( 'Done' ) // 1 call, 1 log
}
// Auto wrap method call to sub-task
@ $mol_wire_method
log( ... args: any[] ) {
console.log( ... args )
// No restarts because console api isn't idempotent
}
- Вводится понятие атома, как многоразового волокна, автоматически обновляющего кеш при изменении зависимостей.
- Раскрывается механизм взаимодействия разного типа волокон друг с другом.
- Приводится пример использования задач для борьбы с неидемпотентностью обращений к атомам, меняющих своё состояние динамически.
@ $mol_wire_method
toggle() {
this.completed( !this.completed() ) // read then write
}
@ $mol_wire_solo
completed( next = false ) {
$mol_wait_timeout( 1000 ) // 1s debounce
return next
}
- Подчёркивается слабое место абстракции каналов — возможное нарушение инвариантов при проталкивании.
- Разбираются различные стратегии поведения при противоречии результата проталкивания инварианту: авто предзатягивание, авто постзатягивание, ручное затягивание.
- Рассматриваются альтернативные более строгие абстракции.
- Обосновывается выбор наиболее простой стратегии, минимизирующей накладные расходы и максимизирующей контроль прикладным программистом.
@ $mol_wire_solo
left( next = false ) {
return next
}
@ $mol_wire_solo
right( next = false ) {
return next
}
@ $mol_wire_solo
res( next?: boolean ) {
return this.left( next ) && this.right()
}
- Приводятся 5 состояний в которых может находиться волокно: вычисляется, устаревшее, сомнительное, актуальное, финализировано.
- Раскрывается назначение курсора для представления состояния жизненного цикла волокна.
- Иллюстрируются переходы состояний узлов реактивного графа при изменениях значений и при обращении к ним.
- Обосновывается перманентная актуальность значения, получаемого от атома.

- Раскрывается механизм автоматического обновления от точки входа, гарантирующего корректный порядок вычислений.
- Обосновывается отложенный пересчёт инвариантов именно при следующем фрейме анимации, что экономит ресурсы без видимых артефактов.

- Рассматриваются основные сценарии работы с атомами, которые могут зависеть от глубины зависимостей.
- Разбираются два основных подхода к реализации этих сценариев: цикл и рекурсия.
- Обосновывается выбор именно рекурсивного подхода не смотря на его ограничение глубины зависимостей.
- Приводится пример анализа стектрейса и подчёркивается важность его информативности.
- Разъясняется прозрачное поведение реактивной системы для всплывающих исключений.

- Классифицируются возможные значения волокна: обещание, ошибка, корректный результат.
- Классифицируются возможные способы передачи нового значения волокну:
return
, throw
, put
.
- Обосновывается нормализация поведения волокна независимо от способа передачи ему значения.
- Рассматриваются особенности работы с асинхронными и синхронными интерфейсами.
- Разъясняется механизм работы SuspenseAPI, основанный на всплытии обещаний.
- Разбираются возможности отслеживания зависимостей в синхронных функциях, асинхронных и генераторах.
- Приводятся результаты замера скорости работы разных подходов.
- Подчёркивается проблема разноцветных функций и необходимость их обесцвечивания.
- Обосновывается выбор именно синхронного подхода.
something(): string {
try {
// returns allways string
return do_something()
} catch( cause: unknown ) {
if( cause instanceof Error ) {
// Usual error handling
}
if( cause instanceof Promise ) {
// Suspense API
}
// Something wrong
}
}
- Вводятся прокси
$mol_wire_sync
и $mol_wire_async
позволяющие трансформировать асинхронный код в синхронный и обратно.
- Приводится пример синхронной, но не блокирующей загрузки данных с сервера.
function getData( uri: string ): { lucky: number } {
const request = $mol_wire_sync( fetch )
const response = $mol_wire_sync( request( uri ) )
return response.json().data
}
- Разбирается сценарий, когда одно и то же действие запускается до завершения предыдущего запуска.
- Раскрывается особенность
$mol_wire_async
позволяющая управлять будет ли предыдущая задача отменена автоматически.
- Приводится пример использования этой особенности для реализации debounce.
button.onclick = $mol_wire_async( function() {
$mol_wait_timeout( 1000 )
// no last-second calls if we're here
counter.sendIncrement()
} )
- Рассматриваются существующие в JS механизмы отмены асинхронных задач.
- Объясняется как использовать механизм контроля времени жизни в том числе и для обещаний.
- Приводится пример простейшего HTTP загрузчика, способного автоматически отменять запросы.
const fetchJSON = $mol_wire_sync( function fetch_abortable(
input: RequestInfo,
init: RequestInit = {}
) {
const controller = new AbortController
init.signal ||= controller.signal
const promise = fetch( input, init )
.then( response => response.json() )
const destructor = ()=> controller.abort()
return Object.assign( promise, { destructor } )
} )
- Разбирается наивная реализация конвертера температур с циклической зависимостью.
- Реализуется корректный конвертер температур без циклических зависимостей путём выноса источника истины в отдельный атом.
- Раскрывается техника снижения алгоритмической сложности через реактивную мемоизацию на примере вычисления чисел Фибоначчи.

- Рассматриваются проблемы транзакционности работы со внешними состояниями, не поддерживающими изоляцию, на примере личных заметок и локального хранилища.
- Подчёркивается важность не только внутренней согласованности, но и согласованности со внешними состояниями.
- Раскрываются проблемы обмана пользователя, которые только усугубляют ситуацию с которой призваны бороться.
- Обосновывается бесперспективность отката уже принятых изменений и неизбежность несогласованности внешнего состояния.
- Принимается решение не морочить прикладному программисту голову, а сконцентрироваться на том, чтобы он лучше понимал, что происходит.
- Предлагается писать прикладную логику, нормализующую неконсистентность исходных данных.
- Приводятся результаты замеров скорости и потребления памяти
$mol_wire
в сравнении с ближайшим конкурентом MobX
.
- Раскрываются решающие факторы позволяющие
$mol_wire
показывать более чем двукратное преимущество по всем параметрам не смотря на фору из-за улучшенного debug experience.
- Приводятся замеры показывающие конкурентоспособность
$mol_wire
даже на чужом поле, где возможности частичного пересчёта состояний не задействуются.
- Обосновывается важность максимальной оптимизации и экономности реактивной системы.

- Приводятся основные архитектурные проблемы ReactJS.
- Вводятся такие архитектурные улучшения из $mol как controlled but stateful, update without recomposition, lazy pull, auto props и другие.
- Большая часть проблем решается путём реализации базового ReactJS компонента с прикрученным
$mol_wire
.
- Реализуется компонент автоматически отображающий статус асинхронных процессов внутри себя.
- Реализуется реактивное GitHub API, не зависящее от ReactJS.
- Реализуется кнопка с индикацией статуса выполнения действия.
- Реализуется поле ввода текста и использующее его поле ввода числа.
- Реализуется приложение позволяющее вводить номер статьи и загружающее с GitHub её название.
- Демонстрируется частичное поднятие стейта компонента.
- Приводятся логи работы в различных сценариях, показывающие отсутствие лишних ререндеров.

- Обосновывается отсутствие пользы от ReactJS в реактивной среде.
- Привносится библиотека
mol_jsx_lib
осуществляющая рендер JSX напрямую в реальный DOM.
- Обнаруживаются улучшения в гидратации, перемещении компонент без ререндера, доступа к DOM узлам, именовании атрибутов и тд.
- Демонстрируются возможности каскадной стилизации по автоматически генерируемым именам классов.
- Приводятся замеры показывающие уменьшение бандла в 5 раз при сопоставимой скорости работы.

- Приводятся основные архитектурные проблемы DOM.
- Предлагается proposal по добавлению реактивности в JS Runtime.
- Привносится библиотека
mol_wire_dom
позволяющая попробовать реактивный DOM уже сейчас.

- Обосновывается необходимость ленивого построения DOM для заполнения лишь видимой области страницы.
- Подчёркивается сложность виртуализации рендеринга DOM как на уровне фреймворка, так и на прикладном уровне.
- Предлагается стратегии по продвижению реактивности в стандарты.

- Уменьшается объём кода приложения в несколько раз путём отказа от JSX в пользу всех возможностей $mol.
- Отмечается также и расширение функциональности приложения без дополнительных движений.

Итого, введя простую, но гибкую абстракцию каналов, мы проработали множество паттернов их использования для достижения самых разных целей. Единожды разобравшись в этом, мы можем создавать приложения любой сложности, и весело интегрироваться с самыми разными API.
Добавление каналам реактивной мемоизации с автоматической ревалидацией, освобождением ресурсов и поддержкой асинхронности, дало нам как радикальное упрощение прикладного кода, так и повышение его эффективности в потреблении ресурсов процессора и памяти.
А для тех, кто по каким-либо причинам ещё не готов полностью переходить на фреймворк $mol, мы подготовили несколько независимых микробиблиотек:
-
$mol_key (1 KB) — уникальный ключ для структур
-
$mol_compare_deep (1 KB) — быстрое глубокое сравнение объектов
-
$mol_wire_pub (1.5 KB) — минимальный издатель для интеграции в реактивный рантайм
-
$mol_wire_lib (7 KB) — полный набор инструментов для реактивного программирования
-
$mol_wire_dom (7.5 KB) — магия превращения обычного DOM в ReactiveDOM.
-
$mol_jsx_view (8 KB) — по настоящему реактивный React.
Хватайте их в руки и давайте зажигать вместе!
- Приводятся реальные кейсы, где $mol хорошо себя показал в скорости обучения, разработки, запуска, отзывчивости и даже в уменьшении размера команды с сохранением конкурентоспособности.
- Раскрываются основные достоинства разрабатываемой нами на его основе оупенсорс веб-платформы нового поколения.
- Освещаются радужные перспективы по импортозамещению множества веб-сервисов на новом уровне качества.
- Подробно разбираются уже начатые нами проекты, написанные наукоёмкие статьи и записанные хардкорные доклады.
- Предлагается дать денег на продолжение этого банкета или самим начать готовить закуски.
Feedback

Эх, мой технический уровень всё ещё недостаточен для Хабра..
Плюсанул не читая.
Прочитал не плюсанув.
Наблюдаю манию изобретательности.
Почему она неуместна? Потому что масштаб изобретений никакой.
Когда человек пишет программу, ему приходится решать простенькие задачки типа "куда сохранить ввод" или "как найти ранее введённое". И если у человека есть мания, то он обязательно начинает изобретать очень умные подходы к решению этих простеньких задач. Так наизобретали кучу всяких ангуляров с реактами, задумавшись однажды над экзистенциальным вопросом "а не выделить ли нам Action-ы?". И вот все экшены уже выделены, но аппетит ведь приходит во время еды, ну и теперь пришёл черёд задачкам типа "как бы мне покороче записать операцию присваивания", да с реактивностью, и разумеется, в функциональном стиле, ну и ещё с миллионом очень умно выглядящих английских слов (для тех, кто не знает их перевод).
Ну ладно, пусть увлечённые манией маньяки развлекаются. Разве мы против? Только вот дети смотрят на такие игры и некоторые могут заразиться... Но дети, подумайте о смысле. Операция присваивания и так уже очень короткая. Зачем к ней прикручивать так называемый "фрэймворк"? Хотя да, тогда время улетает незаметно и ощущение собственной крутости растёт не по дням, а по часам. Правда возьмись вы конкурировать на сайте фриланса за заказы... В общем - не всё то круто, что блестит.
Все-таки там цель не в укорачивании записии операции присванивания.
И самой концепции "вызов без аргументов - геттер, вызов с аргументом - сеттер", уже много лет - https://api.jquery.com/val/
Когда пишешь счетчик с одной кнопкой и выводом значения на экран, возможно данное объяснение уместно. А когда приходится удерживать в голове какие-нибудь каскады данных со сложными зависимостями и множественными источниками, то начинаешь замечать, что из множества решений простеньких задач по типу получить/сохранить/вывести, простенькое решение общей задачи по работе со всем этим как-то не обнаруживается.
Если честно я не знаю специфики работы на фрилансе, но подозреваю, что если имеется какой-то более менее сложный проект, где порядок различных сущностей хранящихся в бд исчисляется сотнями, то оформлять заказ для фрилансеров на такой проект не самый эффективный способ его реализации.