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


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


Main Aspects of Reactivity

Я очень рекомендую прочитать сперва ту статью, чтобы лучше понимать дальнейшее повествование, где мы с нуля разработаем совершенно новую TypeScript реализацию, вобравшую в себя все самые крутые идеи, позволяющие достигнуть беспрецедентной выразительности, компактности, скорости, надёжности, простоты, гибкости, экономности..


Вторая стадия принятия мола в своё сердце: всё ещё пригорает, но уже не можешь остановиться.


Статья разбита на главы, перелинкованные с соответствующими аспектами из упомянутого выше анализа. Так что если вдруг потеряетесь — сможете быстро восстановить контекст.


Повествование будет долгим, но если вы продержитесь до конца, то сможете смело идти к начальнику за повышением. Даже если вы сам себе начальник.


К сожалению, статья получилась слишком длинной для Хабра, поэтому полную версию вы найдёте среди остальных статей. Далее же я подготовил для вас краткое оглавление, чтобы вы понимали сколько всего вас там ждёт.


Origin


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

let _title = ''
const title = ( text = _title )=> _title = text

title()                  // ''
title( 'Buy some milk' ) // 'Buy some milk'
title()                  // 'Buy some milk'

Property


  • Рассматривается использование каналов в качестве методов объектов.
  • Вводится декоратор $mol_wire_solo, мемоизирующий их работу для экономии вычислений и обеспечения идемпотентности.

class Task extends Object {

    @ $mol_wire_solo
    title( title = '' ) {
        return title
    }

    details( details?: string ) {
        return this.title( details )
    }

}

Recomposition


  • Рас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
    }

}

Multiplexing


  • Рассматриваются каналы, мультиплексированные в одном методе, принимающем первым аргументом идентификатор канала.
  • Вводится новый декоратор $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'

Keys


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

@ $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 )
}

Factory


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

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
    }

}

Hacking


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


Binding


  • Связывание объектов классифицируются по направлению: одностороннее и двустороннее.
  • А так же по методу: делегирование и хакинг.
  • Подчёркивается недостатки связывания методом синхронизации.

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 )
    }

}

Debug


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


Fiber


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


Publisher


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

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
}

Dupes


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


Subscriber


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

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()

}

Task


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

}

Atom


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

@ $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
}

Abstraction Leakage


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

@ $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()
}

Tonus


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


Order


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


Depth


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


Error


  • Классифицируются возможные значения волокна: обещание, ошибка, корректный результат.
  • Классифицируются возможные способы передачи нового значения волокну: return, throw, put.
  • Обосновывается нормализация поведения волокна независимо от способа передачи ему значения.

Extern


  • Рассматриваются особенности работы с асинхронными и синхронными интерфейсами.
  • Разъясняется механизм работы 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
    }

}

Recoloring


  • Вводятся прокси $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
}

Concurrency


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

button.onclick = $mol_wire_async( function() {
    $mol_wait_timeout( 1000 )
    // no last-second calls if we're here
    counter.sendIncrement()
} )

Abort


  • Рассматриваются существующие в 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 } )

} )

Cycle


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


Atomic


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

Economy


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


Reactive ReactJS


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


Reactive JSX


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


Reactive DOM


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


Lazy DOM


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


Reactive Framework


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


Results


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

Хватайте их в руки и давайте зажигать вместе!



Growth


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


Feedback



Эх, мой технический уровень всё ещё недостаточен для Хабра..




Комментарии (5):

  1. i360u
    /#24467622 / +1

    Плюсанул не читая.

  2. karambaso
    /#24468266 / -3

    Наблюдаю манию изобретательности.

    Почему она неуместна? Потому что масштаб изобретений никакой.

    Когда человек пишет программу, ему приходится решать простенькие задачки типа "куда сохранить ввод" или "как найти ранее введённое". И если у человека есть мания, то он обязательно начинает изобретать очень умные подходы к решению этих простеньких задач. Так наизобретали кучу всяких ангуляров с реактами, задумавшись однажды над экзистенциальным вопросом "а не выделить ли нам Action-ы?". И вот все экшены уже выделены, но аппетит ведь приходит во время еды, ну и теперь пришёл черёд задачкам типа "как бы мне покороче записать операцию присваивания", да с реактивностью, и разумеется, в функциональном стиле, ну и ещё с миллионом очень умно выглядящих английских слов (для тех, кто не знает их перевод).

    Ну ладно, пусть увлечённые манией маньяки развлекаются. Разве мы против? Только вот дети смотрят на такие игры и некоторые могут заразиться... Но дети, подумайте о смысле. Операция присваивания и так уже очень короткая. Зачем к ней прикручивать так называемый "фрэймворк"? Хотя да, тогда время улетает незаметно и ощущение собственной крутости растёт не по дням, а по часам. Правда возьмись вы конкурировать на сайте фриланса за заказы... В общем - не всё то круто, что блестит.

    • PavelZubkov
      /#24468726 / +2

      "как бы мне покороче записать операцию присваивания"

      Все-таки там цель не в укорачивании записии операции присванивания.
      И самой концепции "вызов без аргументов - геттер, вызов с аргументом - сеттер", уже много лет - https://api.jquery.com/val/

    • movl
      /#24469098 / +5

      Когда пишешь счетчик с одной кнопкой и выводом значения на экран, возможно данное объяснение уместно. А когда приходится удерживать в голове какие-нибудь каскады данных со сложными зависимостями и множественными источниками, то начинаешь замечать, что из множества решений простеньких задач по типу получить/сохранить/вывести, простенькое решение общей задачи по работе со всем этим как-то не обнаруживается.

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