Pull to refresh

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

Reading time12 min
Views10K

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


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



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

Only registered users can participate in poll. Log in, please.
Дочитали до конца?
21.74% Два раза прочитал, но так ничего и не понял.15
14.49% Читал неделю, но всё понял с первого раза.10
39.13% Не осилил, много букав.27
24.64% Даже читать не буду, ничего ты не понимаешь в реактивах, Джонс Ноу.17
69 users voted. 29 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 23: ↑18 and ↓5+13
Comments15

Articles