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

от автора

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

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'

Style

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

Watch

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

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

Flow

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

Integration

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Дочитали до конца?
21.43% Два раза прочитал, но так ничего и не понял. 3
21.43% Читал неделю, но всё понял с первого раза. 3
42.86% Не осилил, много букав. 6
14.29% Даже читать не буду, ничего ты не понимаешь в реактивах, Джонс Ноу. 2
Проголосовали 14 пользователей. Воздержались 6 пользователей.

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


Комментарии

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

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