Проектируем идеальную систему реактивности

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


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
    }

}


  • Рассматривается техника настройки объекта путём переопределения его каналов.
  • Демонстрируется поднятие стейта используя хакинг.
  • Подчёркиваются преимущества хакинга для связывания объектов ничего не знающих друг про друга.

yjvtx7muae2d04kmyih9rowav9u.png


  • Связывание объектов классифицируются по направлению: одностороннее и двустороннее.
  • А так же по методу: делегирование и хакинг.
  • Подчёркивается недостатки связывания методом синхронизации.
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 для ещё большей информативности объектов в отладчике.
  • Демонстрируется логирование изменений состояний с отображением их идентификаторов.

02ef2e9860f9cdb4e23e8aba43593692.png


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

yjunkfnovo-phcwplk2dd1al1q0.png


  • Вводится понятия издателя, как минимального наблюдаемого объекта.
  • Оценивается потребление памяти издателем.
  • Демонстрируется применение издателя для реактивизации обычной переменной и адреса страницы.
  • Предлагается к использованию микро библиотека, предоставляющая минимального издателя для встраивания в другие библиотеки.
  • Демонстрируется создание реактивного множества из нативного.
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
}


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

_pq6ydh7ajlgkp0rbpmg22xsgm0.png


  • Вводится понятие подписчика, как наблюдателя способного автоматически подписываться на издателей и отписываться от них.
  • Оценивается потребление памяти подписчиком и подписчиком совмещённым с издателем.
  • Раскрывается алгоритм автоматической подписки на издателей.
  • Разбирается ручная низкоуровневая работа с подписчиком.
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 состояний в которых может находиться волокно: вычисляется, устаревшее, сомнительное, актуальное, финализировано.
  • Раскрывается назначение курсора для представления состояния жизненного цикла волокна.
  • Иллюстрируются переходы состояний узлов реактивного графа при изменениях значений и при обращении к ним.
  • Обосновывается перманентная актуальность значения, получаемого от атома.

lwygjpnqagnmaj-i4d75q1vswzo.png


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

r1yonh6-agq4o6eu3f54e07dthy.png


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

4f7b2db6043a015a34e1180bbb3a9447.png


  • Классифицируются возможные значения волокна: обещание, ошибка, корректный результат.
  • Классифицируются возможные способы передачи нового значения волокну: 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 } )

} )


  • Разбирается наивная реализация конвертера температур с циклической зависимостью.
  • Реализуется корректный конвертер температур без циклических зависимостей путём выноса источника истины в отдельный атом.
  • Раскрывается техника снижения алгоритмической сложности через реактивную мемоизацию на примере вычисления чисел Фибоначчи.

ltyz68b-gyfkqbwtubx8pdsvcso.png


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


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

8ba5c03c96caca6d78257c0b1d7aaed7.png


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

uslyear0opiathg8whfifakmjue.jpeg


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

yuybl8xljnakvwhoneotiecqq2e.jpeg


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

zsjiwz4h4mjdpza8il-cnh1qxx8.jpeg


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

fjvtiwjq-txjlaa_t_qm7ndjwio.jpeg


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

36eb84144273d26e531fa1fcf6980f77.png

Итого, введя простую, но гибкую абстракцию каналов, мы проработали множество паттернов их использования для достижения самых разных целей. Единожды разобравшись в этом, мы можем создавать приложения любой сложности, и весело интегрироваться с самыми разными 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 хорошо себя показал в скорости обучения, разработки, запуска, отзывчивости и даже в уменьшении размера команды с сохранением конкурентоспособности.
  • Раскрываются основные достоинства разрабатываемой нами на его основе оупенсорс веб-платформы нового поколения.
  • Освещаются радужные перспективы по импортозамещению множества веб-сервисов на новом уровне качества.
  • Подробно разбираются уже начатые нами проекты, написанные наукоёмкие статьи и записанные хардкорные доклады.
  • Предлагается дать денег на продолжение этого банкета или самим начать готовить закуски.


© Habrahabr.ru