СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Объектное Реактивное Программирование в черновиках

  1. Рекламная пауза
  2. $mol — реактивный до мозга костей
  3. Что будем делать вечером? Попробуем завоевать ритейл!
  4. Каталог товаров
  5. Фильтрация
  6. Сортировка
  7. Учёт всех зависимостей
  8. Всё ли рендерить?
  9. Или не всё?
  10. Отображение лишь видимого
  11. Примерение изменений к DOM
  12. Виртуальный DOM
  13. Прямые зависимости
  14. И так сойдёт!
  15. Треугольник серпинского на ReactJS
  16. Треугольник серпинского с использованием ОРП
  17. Парадигмы
  18. Проталкиваем (ФРП)
  19. Затягиваем (ОРП)
  20. Добавляем сортировку
  21. Отображаем лишь видимое
  22. Реактивный рендеринг
  23. А если исключение?
  24. Защищаем приложение от падения компонента
  25. Индикация ошибки
  26. Загрузка: Синхронная блокирующая
  27. Загрузка: Асинхронная неблокирующая
  28. Загрузка: Синхронная неблокирующая с ручной параллельностью
  29. Загрузка: Синхронная неблокирующая без лишнего шума
  30. Загрузка: Автоматическое распараллеливание
  31. Загрузка: Игрушки
  32. Индикация ожидания
  33. Движение данных
  34. Двунаправленные зависимости
  35. Двунаправленные зависимости: Меняем состояние виджета
  36. Двунаправленные зависимости: Меняем состояние страницы
  37. Двунаправленные зависимости: Меняем состояние модели
  38. Двунаправленные зависимости: Пришёл ответ от сервера
  39. Однонаправленный поток данных
  40. Однона… что это у нас?
  41. Потока хоть и два, но не пересекаются
  42. Двусторонние каналы: Прямой поток
  43. Двусторонние каналы: Обратный поток
  44. Двусторонние каналы: Непротиворечивый цикл
  45. Абстракции
  46. Одностороннее переопределение
  47. Двустороннее переопределение
  48. Это вообще законно?
  49. Контроль времени жизни
  50. Связывание владельца с имуществом
  51. Резюме
  52. Вопросы?

Дмитрий Карловский из SAPRUN представляет… ммм...


Это — текстовая версия одноимённого выступления на FrontendConf'17. Вы можете читать её как статью, либо открыть в интерфейсе проведения презентаций, либо посмотреть видео.

Надоело.. Чем поможет ОРП?
… писать много, а делать мало? Пиши мало, делай много!
… часами дебажить простую логику? Реактивные правила обеспечат консистентность!
… асинхронщина? Синхронный код тоже может быть неблокирующим!
… что всё по умолчанию тупит? ОРП оптимизирует потоки данных автоматом!
… функциональные головоломки? Объекты со свойствами — проще некуда!
… что приложение падает целиком? Позволь упасть его части — само поднимется!
… жонглировать индикаторами ожидания? Индикаторы ожидания пусть сами появляются, где надо!
… двустороннее связывание? Двустороннее связывание нужно правильно готовить!
… пилить переиспользуемые компоненты? Пусть компоненты будут переиспользуемыми по умолчанию!
… вечно догонять? Вырывайся вперёд и лидируй!

Рекламная пауза


SAPRUN


Всем привет, меня зовут Дмитрий Карловский. Я — руководитель группы веб-разработки компании САПРАН. Компания наша является крупнейшим интегратором САП-а в России, но в последнее время мы активно смотрим в сторону разработки собственных программных продуктов.

$mol — реактивный до мозга костей


Один из них — кроссплатформенный open source веб фреймворк быстрого построения отзывчивых интерфейсов с говорящим названием "$mol". В нём мы по максимуму применяем возможности Объектного Реактивного Программирования, о которых я и расскажу далее...

$mol


Что будем делать вечером? Попробуем завоевать ритейл!


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

toys.hyoo.ru


Каталог товаров


Игрушек у нас много, а начать продажи надо было ещё вчера. Поэтому мы хотим сделать всё как можно быстрее, но не про… теряв user experience.

Мы можем загрузить основные данные всех игрушек на клиентское устройство и позволить пользователю просматривать каталог быстро, без сетевых задержек.

Каталог различных игрушек


Фильтрация


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

Каталог с фильтром


На больших объёмах данных сложный фильтр может накладываться продолжительное время, поэтому для обеспечения отзывчивости нам бы не хотелось, чтобы фильтрация повторялась лишний раз, когда в ней нет необходимости.

Например, если мы отфильтровали по размеру, то при изменении числа отзывов нет смысла выполнять повторную фильтрацию. А вот если отфильтровали по числу отзывов… ну вы поняли, да?

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

Сортировка


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

Каталог со сложным фильтром и сложной сортировкой


Очевидно, тут повторную сортировку нужно будет произвести лишь при изменении критерия сортировки… или тех свойств товаров, по которым мы сортировали. Но не любых товаров, а лишь тех, что соответствуют критерию фильтрации. И, соответственно, при изменении этого самого критерия. А также, при изменении свойств товаров, по которым мы фильтровали. И...

Учёт всех зависимостей


Если вы попытаетесь описать в коде все зависимости между всеми состояниями, то произойдёт комбинаторный взрыв и вам оторвёт руки.

Диаграмма всех зависимостей между состояниями


Тут мы описали лишь 2 шага преобразований, но в реальном приложении их может более десятка.

Чтобы обуздать эту экспоненциально растущую сложность, и было придумано Реактивное Программирование. Без него вы не сможете сделать сколь-нибудь сложное приложение быстрым, стабильным и компактным одновременно.

Всё ли рендерить?


Если вы будете отображать все данные, что подготовили, то алгоритмическая сложность рендеринга будет пропорциональна объёму этих данных. 10 товаров рендерятся мгновенно, 1000 — с задержкой, а если 10000, то пользователь успеет сходить попить чайку.

График с линейной прогрессией


Или не всё?


Если у пользователя такой экран, что одновременно в него влезает не более 10 товаров, то визуально для него не будет никакой разницы — будете ли вы рендерить всю 1000 или только 10 из них, а по мере скроллинга дорендеривать недостающее. Поэтому, каким бы быстрым ни был у вас React шаблонизатор, он всегда будет проигрывать по отзывчивости ленивой архитектуре, которая в гораздо меньшей мере зависит от объёмов данных.

График с логарифмической и линейной прогрессией


Отображение лишь видимого


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

Диаграмма вырезания видимой части списка товаров


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

Примерение изменений к DOM


Ок, данные мы подготовили, осталось показать их пользователю. Решение в лоб — удалить старое DOM-дерево и вырастить новое. Именно так работают все HTML-шаблонизаторы.

Перерисовывать DOM - это долго


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

Короче говоря, реальность упорно не хочет быть чистой функцией. Чтобы приложение работало как следует, нужно по возможности изменять существующее состояние, а не просто создавать новое. А если не можешь победить — возглавь!

Виртуальный DOM


Как натянуть ужа на ежа? Правильно, давайте генерировать новое DOM дерево всего приложения, а потом React специальная библиотека будет сравнивать его новую и старую версию, и применять различия к тому DOM дереву, что реально видит пользователь.

Виртуальный дом на каждый чих - это медленно


Звучит как костыль, не правда ли? Сколько работы приходится выполнять только лишь для того, чтобы, изменить значение текстового узла, когда в одной из моделей поменялось строковое свойство.

Прямые зависимости


А как могла бы выглядеть работа наиболее эффективного решения?

Прямые зависимости. Что может быть эффективней?


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

И так сойдёт!


И так сойдёт!


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

Треугольник серпинского на ReactJS


Но на это у меня есть простое соображение. Чтобы выше приложение работало со скоростью в 60 кадров в секунду, то оно должно успевать выполнить все операции, начиная с подготовки данных и заканчивая их отрисовкой, всего за 16 миллисекунд. И превысить их очень легко даже в тривиальном приложении на мощном компьютере.

nin-jin.github.io/sierpinski/stack.html


Перед вами известное демо созданное ребятами из Facebook, показывающее как сильно тупят сложные приложения на Реакте. Они это сейчас пытаются решить размазыванием вычислений по нескольким кадрам, что даёт заветные 60 кадров в секунду, но приводит к визуальным артефактам. Фундаментальная же проблема у них остаётся неизменной — виртуальный DOM требует кучи лишних вычислений на каждый чих.

Треугольник серпинского с использованием ОРП


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

mol.js.org/perf/sierp/


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

Парадигмы


Давайте, наконец, добавим немного теории…

Что такое Объектное Программирование? Основной его чертой является объединение данных и функций для работы с ними в рамках одной абстракции с относительно простым интерфейсом — объекте.

А что такое Функциональное Программирование? Тут вся программа описывается в виде кучи чистых функций, которые не зависят от изменяемого состояния и сами не изменяют никаких состояний.

Наконец, что такое Реактивное Программирование? Здесь вы описываете правила получения одних состояний из других таким образом, что изменение одного состояния приводит к каскадному изменению зависимых.

Объектное, Функциональное и Реактивное


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

Проталкиваем (ФРП)


Есть два принципиально разных способа реализации реактивности.

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

const FilterSource = new Rx.BehaviorSubject( toy => toy.count > 0 )
const Filter = FilterSource.distinctUntilChanged().debounce( 0 )

const ToysSource = new Rx.BehaviorSubject( [] )
const Toys = ToysSource.distinctUntilChanged().debounce( 0 )

const ToysFiltered = Filter
.select( filter => {
    if( !filter ) return Toys
    return Toys.map( toys => toys.filter( filter ) )
} )
.switch()
.distinctUntilChanged()
.debounce( 0 )

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

Тут уже применено несколько типовых оптимизаций. Тем не менее работает этот код очень не эффективно: список игрушек перефильтровывается даже если в товаре поменялись те данные, от которых результат фильтрации не зависит. Чтобы побороть эту проблему, придётся ещё на порядок усложнить код, но мало кто это осилит.

Данный подход приводит к сложному, трудноподдерживаемому коду. Его трудно читать. Его сложно писать. Его лень писать правильно. И в нём легко допустить ошибку, если вы, конечно, не финалист специальной олимпиады по информатике.

Затягиваем (ОРП)


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

class $my_toys {

    @ $mol_mem()
    filter( next ) {
        if( next === undefined ) return toy => toy.count() > 0

        return next
    }

    @ $mol_mem()
    toys( next = [] ){ return next }

    @ $mol_mem()
    toys_filtered() {
        if( !this.filter() ) return this.toys()

        return this.toys().filter( this.filter() )
    }
}

Не правда ли ORP код куда проще, и понятнее? Это тот же код, который мы бы написали безо всякого реактивного программирования, но мы добавили специальный декоратор, который динамически отслеживает зависимости по факту обращения к ним, кеширует результат исполнения функции и сбрасывает кеш, когда зависимости изменяются.

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

Добавляем сортировку


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

@ $mol_mem()
sorter( next ) {
    if( next === undefined ) return ( a , b )=> b.price() - a.price()

    return next
}

@ $mol_mem()
toys_sorted() {
    if( !this.sorter() ) return this.toys_filtered()

    return this.toys_filtered().slice().sort( this.sorter() )
}

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

Отображаем лишь видимое


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

@ $mol_mem()
toys_visible() {
    return this.toys_sorted().slice( ... this.view_window() )
}

Сразу же введём свойство children, которое будет возвращать компоненты, которые мы хотим отрендерить внутри нашего. А рендерить мы хотим лишь попадающие в видимую область.

children() {
    return this.toys_visible()
}

Реактивный рендеринг


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

@ $mol_mem()
render() {
    let node = document.getElementById( this.id() )

    if( !node ) {
        node = document.createElement( 'div' )
        node.id = this.id()
    }

    /// Node updating here

    return node
}

Зная идентификатор компонента, мы ищем соответствующий узел в реальном DOM-дереве. А если не нашли, то создаём его. После этого актуализируем его состояние и возвращаем.

Обратите внимание на реактивныый кеширующий декоратор. Благодаря ему, рендеринг конкретно этого узла будет перезапущен лишь, когда изменится любое из свойств, к которому нам потребовалось обратиться в процессе обновления состояния нашего DOM-узла.

А если исключение?


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

toys.hyoo.ru/#luck=.9


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

Защищаем приложение от падения компонента


Поэтому давайте завернём рендеринг DOM-узла в блок try-catch и в случае возникновения ошибки, записывать имя исключения в специальный атрибут. А если рендеринг пройдёт без эксцессов — стирать его.

try {

    /// Node updating here

    node.removeAttribute( 'mol_view_error' )

} catch( error ) {

    console.error( error )

    node.setAttribute( 'mol_view_error' , error.name )
}

Индикация ошибки


Зачем нам этот атрибут? Да чтобы через CSS стилизовать сломанный блок, показывая пользователю, что он сейчас не работает. Например, мы можем сделать его полупрозрачным и запретить пользователю с ним взаимодействовать.

[mol_view_error] {
    opacity: .5 !important;
    pointer-events: none !important;
}

Загрузка: Синхронная блокирующая


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

namesakes_message() {

    /// Serial
    const user = this.user()
    const count = this.name_count( user.name )

    return this.texts().namesakes_message
    .replace( /\{count\}/g , count )
}

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

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

Загрузка: Асинхронная неблокирующая


Чтобы решить эту проблему в яваскрипте принято писать код на колбэках.

namesakes_message() {

    /// Parallel
    return Promise.all([

        /// Serial
        this.user().then( user => this.name_count( user.name ) ,

        this.texts() ,

    ])
    .then( ([ count , texts ])=> {
        return texts.namesakes_message.replace( /\{count\}/g , count )
    } )

}

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

Загрузка: Синхронная неблокирующая с ручной параллельностью


Недавно в яваскрипте появились средства синхронизации, но действуют они лишь в пределах одной функции, а не всего стека вызовов.

/// Serial
async namesakes_count() {
    const user = await this.user()
    return await this.name_count( user.name )
}

async namesakes_message() {

    /// Parallel
    const [ count, texts ] = await Promise.all([
        this.namesakes_count() ,
        this.texts() ,
    ])

    return texts.namesakes_message.replace( /\{count\}/g , count )
}

Если мы пометили функцию как "асинхронную", то мы можем приостанавливать её до появления определённого события.

Как видно, это не сильно спасает, так как для распараллеливания запросов всё равно приходится кастовать специальные заклинания.

Загрузка: Синхронная неблокирующая без лишнего шума


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

@ $mol_mem()
namesakes_message() {

    /// Parallel
    const texts = this.texts()
    const user = this.user()

    /// Serial
    const count = this.namesakes_count( user.name )

    return texts.namesakes_message.replace( /\{count\}/g , count )
}

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

Загрузка: Автоматическое распараллеливание


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

В данном примере, первое прерывание произойдёт лишь при доступе к user.name, а значит загрузка текстов и информации о пользователе пойдёт параллельно.

@ $mol_mem()
namesakes_message() {

    /// Parallel
    const texts = this.texts()
    const user = this.user()

    /// Serial
    const count = this.namesakes_count( user.name ) /// <-- first yield

    return texts.namesakes_message.replace( /\{count\}/g , count )
}

Загрузка: Игрушки


Давайте наконец добавим загрузку списка игрушек, вместо локального его хранения. Для этого нам потребуется изменить всего один метод.

/// Before
@ $mol_mem()
toys(){ return [] }

/// After
toys_data() {
    return $mol_http.resource( '/toys.json' ).json()
}

@ $mol_mem()
toys() {
    return Object.keys( this.toys_data() ).map( id => this.toy( id ) )
}

Как видите, нам не потребовалось менять его интерфейс — мы просто взяли содержимое файла, как если бы оно было у нас локально, обработали его и вернули результат.

Индикация ожидания


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

[mol_view_error="$mol_atom_wait"] {
    animation: my_waiting .25s steps(6) infinite;
}

Движение данных


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

Взаимодействие пользователя с сервером через интерфейс


На диаграмме, каждое звено — некоторое состояние, выраженное в различных моделях. На бэкенде, например, дата у нас выражена числом в таблице. На фронтенде она уже представляется виджетом "календарик". Ну а в голове у пользователя это просто "тот день, когда я выступал на конференции".

Если опустить посредников, то можно заметить, что источником истины о том, что видит пользователь является бэкенд, а источником истины о том, что следует изменить на бэкенде, является пользователь.

Но самое интересное происходит, когда мы добавляем посредников. Например, пользовательский интерфейс, состояние которого зависит и от сервера и от пользователя. Как же реализовать его так, чтобы работал он чётко и предсказуемо?

Двунаправленные зависимости


Что если пользователь будет менять не исходные данные, а непосредственно то состояние, которое он видит в виджете, на который смотрит?


Двунаправленные зависимости: Меняем состояние виджета



Двунаправленные зависимости: Меняем состояние страницы



Двунаправленные зависимости: Меняем состояние модели



Двунаправленные зависимости: Пришёл ответ от сервера



Как думаете, что за фреймворк тут использован?

Это типичное приложение на Angular. Когда пользователь меняет состояние, начинают срабатывать watcher-ы, которые итеративно синхонизируют состояния между собой, что приводит ко временной неконсистентности состояния клиент-серверной системы вцелом.

Эта логика работы мало того, что не эффективна, так ещё и требует очень аккуратного написания кода с точно подогнанными костылями вида "подождать пока всё синхронизируется, а потом вызвать обработчик".

Однонаправленный поток данных


А как называется архитектура со следующей диаграммы?


Однона… что это у нас?



Потока хоть и два, но не пересекаются



Facebook подумал-подумал и придумал FLUX, где поток от сервера к пользователю идёт через компоненты, а обратно — через глобальные процедуры — так называемые "Экшены".

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

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

Двусторонние каналы: Прямой поток


Проблема двустороннего связывания Ангуляра была не в том, что оно двустороннее, а в том, что состояние сначала изменялось, а потом отложенно синхронизировалось. Причём меняться оно могло как пользователем, так и сервером, что закономерно приводило к конфликтам состояний. Именно в этом был корень всех проблем. Как же его выкорчевать?


Двусторонние каналы: Обратный поток


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


На диаграмме можно видеть, как новое значение передаётся от объекта к объекту не меняя их состояний.

Двусторонние каналы: Непротиворечивый цикл


И только когда приходит ответ от сервера с актуальным значением, оно спускается по иерархии компонент, меняя их состояние.


У этой архитектуры нет проблем Ангуляра с неконсистентностью, ведь прямой и обратный потоки данных не перемешиваются. Но нет и проблем FLUX-а, так как каждый компонент взаимодействует лишь со своим непосредственным владельцем и непосредственным имуществом.

Абстракции


class $mol_string {

    hint() { return '' }

    @ $mol_mem()
    value( next = '' ) { return next }

    // ...
}

Данный пример — компонент строкового поля ввода. Для иллюстрации приведено два свойства: hint — это текст показываемый, если значение поля не задано; и value — это текущее значение. Когда пользователь вводит новое значение оно передаётся в value. А благодаря декоратору, результат работы метода кешируется в внутри объекта.

Одностороннее переопределение


Чтобы настроить поведение компонента мы можем просто переопределить его свойства своими функциями. Например, можем сделать, чтобы hint возвращал известного персонажа комиксов.

const Name = new $mol_string

Name.hint = ()=> 'Batman'

Двустороннее переопределение


Или можем попросить компонент в качестве value брать не своё локальное состояние, а нашу локальную переменную.

let name = 'Jin'

Name.value = ( next = name )=> {
    return name = next
}

Тут мы просто говорим, что при затягивании из value нужно вернуть значение переменной name, а при проталкивании — записывать в name и возвращать актуальное значение.

Это вообще законно?


Вам может показаться, что это дурнопахнущий код, так как мы берём и переопределяем любой метод стороннего объекта, и так делать нельзя.

Кавайная какашка


Но на практике это всё отлично работает и не доставляет проблем.

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

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

Контроль времени жизни


Лучше, конечно, не создавать объекты в воздухе и не разбрасываться локальными переменными, а хранить все объекты в свойствах других объектов. Например, мы можем создать локальную фабрику, которая единожды создаёт и настраивает объект.

    @ $mol_mem()
    Name() {
        const Name = new $mol_string

        /// Setup ```Name``` here

        return Name
    }

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

Связывание владельца с имуществом


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

        /// Setup ```Name```:

        /// One way binding
        Name.hint =  ()=> this.name_hint()

        /// Two way binding
        Name.value = ( next )=> this.name( next )

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

Резюме


Реактивное программирование — каскадное изменение состояний по нашим правилам.


ОРП провоцирует простой, понятный, но эффективный код.


Ленивая архитектура минимизирует объём вычислений.


Потока данных всегда два и они не должны пересекаться.


Синхронный код — добро.


Ручное управление потоками данных — зло.


Напоследок хотелось бы дать совет: не гонитесь за хайпом. Мода переменчива и часто тащит нас в болото. Если ваш единственный аргумент — число разработчиков "знающих" технологию, то готовьтесь к тому, что через пару лет никто из них уже не захочет с ней связываться, а ещё через пару — вы не найдёте никого, кто смог бы разобраться в коде проекта.

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

Вопросы?


На этом у меня всё. Если у вас возникли какие-либо вопросы, я с радостью на них отвечу.

Реализации ОРП: $mol_mem, VueJS, MobX, CellX, KnockOut


Получившийся магазин: toys.hyoo.ru


Исходники магазина: github.com/nin-jin/toys.hyoo.ru


Эти слайды: nin-jin.github.io/slides/orp


Треугольники Серпинского: github.com/nin-jin/sierpinski

комментарии (170)

0
copal ,   * (был изменён)
И о чем статья-то? Мне нужно отфильтровать данные, но это вообще невозможно и все ужасно! Поэтому я применяю реактивный подход, фильтрую и все здорово! Резюме — реактивное программирование, это спасение. Вопросы?
Вот что такое реактивный подход в однопоточном js программировании?
0
vintage ,  

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

+2
copal ,  
Если человек работающий с данными не знает что их можно фильтровать, то ему действительно будет очень полезна эта статья. А в остальном объектно реактивное программирование, это настолько же глупо звучит, как например объектно событийное программирование. Но если второе уже все понимают, то это не значит что более понятно было бы объяснять это как парадигму, максимум архитектура. Зачем все усложнять. Это всего-навсего несколько шаблонов проектирования, которые выполняют команды, которые теперь называют операторами, которые в свою очередь выполняю задачи, которые здесь называют просто функции.
И на мой взгляд если действительно понимать о чем идет речь, то за подобный объем можно четыре раза рассказать ИСЧЕРПЫВАЮЩЕ что это такое.
+6
MrCheater ,  

Что-то http://toys.hyoo.ru/ безбожно тормозит при скролинге. Какая-то анти-реклама $mol

–8
+1 –9
vintage ,  

А вы не за скроллбар дёргайте.

+3
+4 –1
mayorovp ,  

А зачем тогда он нужен-то?

–3
+1 –4
vintage ,  

А тут он и не нужен, собственно. На мобилках он и не показывается.

+2
+3 –1
mayorovp ,   * (был изменён)

Надо тогда и на десктопе тоже его не показывать.


Лучше объясните, что там вообще такого накручено, что дерганье за скролбар приводит к зависанию сайта. И зачем.

0
vintage ,  

Если б ещё был безкостыльный способ его скрыть.


Там просто более 9000 товаров. Если вы захотели странного и умотали в самый конец списка, то получаете рендеринг всех товаров.

0
mayorovp ,  

Нет, у меня повисла вкладка при попытке сдвинуть ползунок скролбара.

0
vintage ,  

Браузер-то какой?

+3
punkkk ,  
в хроме повторяется 100%
+1
Druu ,  
А зачем вообще вы пытаетесь выводить на одной странице 9000 товаров? Какова вероятность того, что пользователь промотает хотя бы до 1000? Вы всерьез можете представить ситуацию, как пользователь упорно сидит и мотает 5 минут страницу???
–1
vintage ,  

А думаете ему будет удобнее 50 раз кликнуть на "следующую страницу"?

0
youlose ,  
Удобнее будет отфильтровать или перейти к нужной категории товара, представьте что амазон выведет миллионы своих товаров на страницу, как от этого толк вообще?
0
vintage ,  

Для этого там и добавлены фильтры.

0
youlose ,  
Пролистал сайт процентов на 40, памяти выделился гигабайт и не освобождается.
+1
faiwer ,  

Ещё бы, вы запустите в консоли после этого $$('[my_toys_catalog_toy_card]').length и посмотрите сколько товаров на самом деле отображается на странице. В итоге тормозит всё: hover, открытие\закрытие панели товара, подсветка выбранного элемента.

+3
Druu ,   * (был изменён)
Я не представляю такой ситуации, когда пользователь упорно проматывает тысячи товаров — не важно, в каком виде. Хоть скроллом, хоть с пагинацией, как угодно.

Очевидно, пользователь сперва применит какие-то фильтры, а потом, когда товаров останется ~несколько сотен, уже и будет просматривать.

Выходит, что:
1. в том случае, когда у вас 9к товаров, виртуальный скролл не нужен, потому что пользователь все равно не будет скроллить
2. а когда товаров несколько сотен — виртуальный скролл не нужен, потому что их можно подгрузить целиком

В итоге получается что какая-то ненужная (в данном конкретном случае, я не говорю, что виртуальный скролл вообще никогда не нужен) фича.

Ну и еще на десктопах меня лично виртуальный скролл всегда бесит — с ним, по очевидным причинам, не работает поиск
0
vintage ,   * (был изменён)

Эта фича позволяет быстро показать страницу, не парясь по поводу количества найденных фильтром товаров.


А браузерный поиск не нужен, если есть соответствующий фильтр по тексту.

+2
Druu ,   * (был изменён)
> Эта фича позволяет быстро показать страницу, не парясь по поводу количества найденных фильтром товаров.

То есть вы сперва выдумали проблему (зачем-то решили показывать пользователю 9к товаров), а потом ее героически преодолели, не показывая их сразу, а показывая по-немногу.

> А браузерный поиск не нужен, если есть соответствующий фильтр по тексту.

Я когда захочу что-то найти на странице, то точно не стану искать какой-то там фильтр по тексту, а нажму ctrl+f. И когда поиск ничего не найдет — то в первую очередь подумаю, что на странице просто и нет соответствующих данных, а не то, что они не подгружены из-за виртуального скролла.
0
vintage ,   * (был изменён)

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


И как паджинация поможет вам "искать по странице нативными средствами"? Или что вы предлагаете? Рендерить все 10000 товаров пару минут, чтобы нативный поиск работал?

+1
faiwer ,  
И как паджинация поможет вам "искать по странице нативными средствами"?

facepalm.jpg. Элементарно, Ватсон. Пользователь видя постраничную навигацию понимает в каком диапазоне будет работать нативный поиск. Он понимает, что поиск затронет только текущую страницу. И пожелай он искать среди всех страниц, как раз пойдёт искать ваше "огромное поле поиска" (по сути от безысходности). Это будет очевидный интерфейс. И тормозить он тоже не будет.

–1
vintage ,  

Зачастую постраничная навигация находится внизу страницы, так что ничего пользователь не видит.

+1
mayorovp ,  

Давайте все-таки сравнивать с лучшими образцами, а не с худшими?


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

0
morikvendy ,  
Даже если постраничная навигация находится внизу страницы, пользователь ее увидит, дернув, например, за тот самый скроллбар, который у Вас заставляет браузер задуматься на пару минут. Я часто на незнакомых сайтах именно так и делаю — прокрутил страницу вниз и вижу либо что выведены все товары, тогда можно нажать ctrl+f и найти что-то конкретное, либо увидеть что еще 100500 страниц есть, тогда пытаемся использовать поиск сайта. более того, постраничный вывод тем и хорош, что там немного товаров и одна страница просматривается глазами за минуту максимум и редко занимает больше двух экранов, так что пользователь все равно с высокой вероятностью доскролит до пагинатора.
P.S.: http://toys.hyoo.ru — вывалился в ошибку сценария через минуту игры с фильтрами (браузер FireFox 53.0.3).
0
vintage ,  

Текст ошибки вы, конечно, не приведёте?

0
morikvendy ,  
Вы, видимо, уменьшили количество элементов на странице, теперь их всего 1000, так что воспроизвести было немного сложнее, но все равно:
Похоже, исполняемый на этой странице сценарий занят или не отвечает. Вы можете остановить его сейчас, открыть сценарий в отладчике или позволить сценарию продолжить свою работу.

Сценарий: http://toys.hyoo.ru/-/web.js:675

При нажатии на подождать, сайт все равно не отвечает, ну а при остановке, все фильтры умирают)
0
vintage ,  

Вы просто переключали фильтры или ещё что-то делали?

0
morikvendy ,  
Сначала немного поскроллил, потом взялся за фильтры.
0
faiwer ,  
то в первую очередь подумаю, что на странице просто и нет соответствующих данных

Вот-вот. С точки зрения обычного пользователя все товары на странице есть, вот же они, можно пальцем в экран ткнуть и показать. Откуда пользователю знать что тут какая-то тормо-магия применена :)

0
vintage ,  

Пользователь не слепой, чтобы не увидеть огромное поле поиска. Мало кто знает про комбинацию ctrl+f, а на мобилке нативный поиск ещё сложнее отыскать.

0
faiwer ,  

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

0
vintage ,  

Видимо у нас с вами разные пользователи. Наши пользователи знать не знают про браузерный поиск по странице. Даже я, программист, предпочитаю поиск от разработчиков приложения, а не браузера, так как он работает куда более адекватно. Разработчики приложения куда лучше знают по каким полям (не обязательно видимым) нужно искать и как это делать.

0
faiwer ,  

Ну-ну, я уже и забыл когда последний раз встречал в кастомных подсветку найденного :)

+1
Druu ,   * (был изменён)
А я вот только неделю назад заметил, что на хабре есть поле поиска. Надо, оказывается, кликнуть на лупу. До этого я думал, что поиска нет кроме как на https://habrahabr.ru/search/

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

Это, разумеется, баг. Панель инструментов по умолчанию должна быть открыта. Залил патч.

0
faiwer ,  
не парясь по поводу количества найденных фильтром товаров

Ваш пример полное тому опровержение. Даже на сильном desktop-компьютере это работает очень медленно и отъедает очень много памяти.


А браузерный поиск не нужен, если есть соответствующий фильтр по тексту.

Не забудьте об этом пользователю написать в большой красной панели, чтобы он знал, что поиск ему не нужен. Пользователь то не в курсе. Он видит большую scroll-область, скроллит и видит на экране элементы. Ему и в голову не придёт, что их нет на самом деле. В случае какой-нибудь ужасной infinitive-ленты это хотя бы очевидно, т.к. и скролл меняет своё поведение по мере загрузки, и анимации loader-ов отображаются.

0
vintage ,  

Вы кейсы не перемешивайте. Память съедается лишь при дорендеривании, что лучше, чем рендерить всё сразу.

+1
faiwer ,  

Я зашёл на страницу, зыркаю на картинки и ищу чего бы мне купить. Игрушки всякие, ищу что-то интересно. Плавно кручу колесо и получаю все вышеописанные прелести. И да, когда я искал подарок на 8 марта я именно так и делал. Какие такие кейсы я перемешиваю?


Если я зашёл на вашу страницу и не стал скроллить вниз, то либо я нашёл что искал сразу (маловероятно, но бывает), либо я вообще оказался здесь зря. Это единственные use-case-ы которые представляют для вас интерес?

0
daemonhk ,  
AJAX-подгрузка OVER9000 товаров при скроллинге, не?
0
vintage ,  

Я не понял вашу мысль.

0
kenik ,  
Подгружать следующую 1000 при пролистывании, скажем, 70% из этой 1000?
–1
vintage ,  

Ну а тут подгружаются следующие 5 при пролистывании до 99%.

0
copal ,  
Вы точно даже представления не имеете о том, о чем пытались здесь написать.
0
vintage ,  

Я вообще идиот, не разговаривайте со мной.

0
Spiritschaser ,  
А почему не сделать AJAX пажинацию, с сохранением страницы в url, и симуляции листания на мобильном устройстве: в конце прокручивания вниз, переходим на следующую страницу, и плавно прокручиваем вверх.

Поэкспериментировав с div'ми и эффектами, можно добиться того, что UX будет примерно как при бесконечном скролле, при этом не нужно хранить в несчастном дереве весь каталог.
0
vintage ,  

Если постоянно прокручивать страницу вверх-вниз, то у пользователя быстро разовьётся морская болезнь. У меня есть одна интересная идея, которой ещё нигде не встречал — что-то типа двустороннего бесконечного скролла. Но руки пока не дошли до его реализации.

+1
raveclassic ,  
А при нажатии колесиком тоже ничего не происходит.
–3
vintage ,  

Это из-за того, что все карточки товаров — это ссылки, а нажатие колёсика на ссылке приводит к открытию ссылки в новой вкладке, но если вы сдвигаете мышь, то открытие отменяется.

0
amaksr ,  
Что он так долго делает?
0
vintage ,   * (был изменён)

Скорее всего вы умотали вниз, а потом перезагрузили страницу — он рендерит до того же места. Правда смещение скролла не восстанавливается. Пока что красивого решения этой проблемы не придумали. Есть костыль с проверкой изменений в дереве, но он тормозит. Ну и есть вариант вообще отказаться от фичи с восстановлением скролла при перезагрузке.

0
amaksr ,  
Да, точно, умотал вниз, потом перезагрузил, так как тормоза от перезагрузки обычно проходят. Но вообще у меня вроде быстрый десктоп. Может не стоит и пытаться рендерить слишком много, или сделать какое-то ограничение по-времени?
0
vintage ,  

Думаю тут вообще не надо восстанавливать позицию скролла.

0
amaksr ,   * (был изменён)
А надо ли рендерить то, что перед текущей страницей? Может просто поместить туда один пустой div с высотой в сколько-то страниц, и рендерить там только в случае если пользователь листает вверх.
0
vintage ,  

В сколько страниц? Пока не отрендеришь — и не узнаешь.

+1
amaksr ,  
У вас вроде все элементы одинаковые, зная кол-во элементов высоту можно вычислить.
Если немного разные, то можно примерно прикинуть среднее. На больших скроллах ошибка будет незаметна.
Если сильно разные, то можно делать анализ предположительно занимаемого места еще на сервере, и просто добавлять этот параметр в ответ сервера, наряду с названием, ценой и т.п. Ошибка на больших скроллах будет незаметна, а маленькие можно просто рендерить как есть.
0
vintage ,  

Нет, тут все разные.


Это всё работает для отсекания снизу. Для отсекания сверху не годится иначе будут скачки.

+3
amaksr ,  
Скачки лучше чем тормоза.
0
vintage ,  

Я уже отключил восстановление смещения скролла. Можете поиграться.

–1
copal ,  
Прикольно! А как Вы писали библиотеку, которая, как раз, должна подобные проблему решать?
0
vintage ,  

Как бы вы её решили?

–1
copal ,  
С такими примитивными вопросами на тостер.
0
bromzh ,  

В сафари сразу же начинает ужасно тормозить при скроле. Даже когда не в самом конце списка товаров. И нет, скроллбар я не трогаю.

0
vintage ,  

Подозреваю, что это из-за отсутствия поддержки пассивных обработчиков событий, из-за чего скроллинг выполняется не в отдельном потоке. Какая у вас версия Сафари и Оси? Лучше сразу писать в задачу: https://github.com/eigenmethod/mol/issues/225

0
bromzh ,   * (был изменён)

Эх, промахнулся веткой(
Тормоза в сафари, версии: MacOS Sierra 10.12.5 (16F73), Safari 10.1.1 (12603.2.4).
В issue тоже отписался.
Треугольник, кстати, не тормозит. А так вообще замечал за сафари подобные тормоза, причём хром и фф не тормозили на тех местах.

+3
faiwer ,  

Ваша реализация вирт. скролла безумно тормозит при попытке проскроллить на величину превышающую высоту экрана. Коль скоро вы его так всюду пиарите, дык доведите до ума. Этим сейчас в дэсктопном браузере просто невозможно пользоваться. $$('[my_toys_catalog_toy_card]').length === 3793 — WTF? Это вы называете виртуальным скроллом? Да сделать нормальный виртуальный скролл для элементов произвольной высоты задача не простая, у меня недели 2 ушло (правда там задача в стократ сложнее, чем просто список), но это же не повод в продакшн выдавать вот такую дичь.

0
vintage ,  

Это и не "вирт скролл". Виртскролл тут: http://mol.js.org/#demo=mol_grid_demo

0
faiwer ,  

Да не суть важно как вы это называете. Главное, что в таком виде, такому подходу место разве что на свалке. А вы его ещё и пиарите, эксцентричные бенчмарки рисуете, которые меряют каких-то никому не интересных попугаев, зато "из коробки". Но, дайте угадаю, это всё потому, что за вами нет гигантской корпорации вроде Google или Facebook-а :)

–3
vintage ,   * (был изменён)

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

0
faiwer ,   * (был изменён)
У каждого подхода есть преимущества и ограничения

Заинтриговали. А какие?


Скроллить больше, чем на высоту экрана за раз — не реалистичный сценарий.

Я машинально это сделал только открыв страницу. Так поступают многие. Да я даже эту страницу на хабре так скроллю. Так же в 100 крат быстрее.

–3
vintage ,  

Возможность динамически менять размеры элементов любым способом, например.


Боюсь люди, которые приходят в интернет-магазин поскроллить страницу, а не выбрать товар — не являются его целевой аудиторией.

+3
faiwer ,  

Что мы имеем по факту:


  1. на малом количестве элементов ― выигрыш по производительностью ничтожен, но при этом ещё и сломан нативный поиск по странице (что, кстати, совершенно неочевидно для пользователя)
  2. на большом количестве элементов ― всё тупо дико тормозит и отжирает ОЗУ

Идём дальше, наш пользователь пришёл на страницу интернет магазина и хочет что-то купить. Следуя вашей логике, он либо покупает что-то сверху, либо закрывает страницу? А если не закрывает, но ищет (и скроллит разумеется), то пусть тогда у него всё тормозит? Я вот вспоминаю, как пользуюсь интернет-магазинами, дык я зачастую нахожу что-нибудь на 5-6 странице того же алиэкспресса, яндекс-маркета и десятка других магазинов. Может полчаса уйти прежде чем найду искомое.


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

0
vintage ,  

О том и речь. 5-6 страница — это обычно порядка сотни-полутора товаров (и нет, рендерится эта сотня на хилых китайских тапках далеко не мгновенно). Если пользователь ничего не нашёл, то зачастую он бросает это дело — либо уходит, либо берётся за фильтры. Именно поэтому до рендеринга овер 9000 товаров никто не доходит. А тормозит в примере магазина при огромном числе отрендеренного из-за обновления каждого товара каждую секунду, добавленного опять же для примера, ибо реально товары не так часто обновляются.


Очень жаль, что вы не слышите о чём я говорю. Вот смотрите:


toy_cards() {
    return this.toys_sorted().map( toy => this.Toy_card( toy.id() ) )
}

Это — весь код рендеинга карточек. Эквивалентный код на Реакте или любом другом фреймворке будет безбожно тормозить.А ведь именно такого рода код пилится в 99% случаев, потому что "он проще" и "да у нас не будет тут много данных" и "на моём i7 всё и так быстро рендерится" и "прикручивание виртуального скролла — отдельная задача, на которую нет времени". Ленивый рендеринг позволяет работать быстро в 90% кейсов даже если данных много, а программист не запаривался над оптимизацией.

+1
faiwer ,  
Эквивалентный код на Реакте или любом другом фреймворке будет безбожно тормозить

Вы упорно игнорируете тот факт, что никто (в здравом рассудке) не будет рендерить 9000 элементов на React-е разом. Тут либо виртуальный скроллинг задействуют (что вряд ли, т.к. тут много скользких моментов), либо какую-нибудь навигацию влепят.


Вы же зачем то заставляете свой фреймворк их рендерить. И так как нормальный виртуальный скроллинг вы не осилили, мы получили жутко тормозную вакханалию. Как яркий пример того, что нет никаких серебрянных пуль и типовых решений, которые можно тупо взять и юзать, без мозгов.


Т.е. резюмируя: ленивый рендеринг хорош тогда, когда его готовят с умом. А не это нечто.


это обычно порядка сотни-полутора товаров

Тут вам нужно пол статьи снести и большими красными буквами написать: моё решение имеет смысл если в вашем списке от 50 до 150 товаров. При больших масштабах он дико тормозит, при меньших просто не имеет смысла. Но даже при количестве 50-150 элементов вы получите неочевидный мёртвый поиск. Вива кривым ленивым решениям.


И последнее. Вы делаете очень много сильных предположений о том, как ведут себя пользователи, и как себя не ведут. И, по моему мнению, практически всегда ошибаетесь. Хотя бы потому, что пользователи очень разные. Это касается не только этого топика, а вообще мои наблюдения.

–1
vintage ,  

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


Ленивый рендеринг хорош, как оптимизация по умолчанию, которая в 90% случаев даёт хороший результат, не требуя дополнительных телодвижений. Если нужно оперативно работать со всеми 9000 элементами, то в этом случае нужно применять уже виртуальный скролл.

+1
faiwer ,  

Вы придумали кривой велосипед и упорно пытаетесь всем доказать, что он не настолько кривой, не всегда кривой, иногда не кривой, и вообще если действовать строго по инструкции и жать "как надо", то даже ещё ничего. Смешно же :)


  1. Простое решение вашей проблемы: оставляете ленивый рендреринг, но лимитируете его неким N, за пределами которого начинается постраничная навигация. Тогда эти ваши, взятые с потолка, 90% пользователей останутся при ленивом рендеринге, а более дотошные юзера хотя бы смогут более-менее нормально пользоваться сайтом.
  2. Сделать что-то вроде как в VK комментарии. Когда постраничная навигация бегает сама, по мере скроллинга, заодно можно url менять. Думаю сделать это хорошо не тривиально, но и не так сложно, как сделать крутой вирт.скролл.

А так, вся эта ситуация, мне напоминает низкие дверные проёмы. В вашем случае по верхней части ещё свисает оголённый 220 вольт провод.

+1
faiwer ,  
А вы упорно игнорируете тот факт, что когда разрабатывают приложения данных обычно сильно меньше.

Кстати говоря, за всю свою практику, ни разу не сталкивался с подобными проблемами. Надо же хоть иногда мозг включать. По умолчанию всегда есть какая-нибудь навигация. Если же её нет, то у заказчика дотошно выясняются допустимые объёмы данных. Вот ни разу не было, чтобы я постфактум что-нибудь прикручивал. Может быть проблема где-то в другом месте?

–1
vintage ,  

В продуктовой разработке у вас нет ТЗ, только статистика, собираемая пост-фактум. Но даже в заказной всё не так радужно с ТЗ, к сожалению.

+1
faiwer ,  

А что для того, чтобы догадаться сделать в списке пагинацию нужно ТЗ? :)

0
+1 –1
vintage ,  

Вы не поверите, но чтобы предположить, что в списке может быть более 50 элементов — нужно ТЗ.

0
+1 –1
mayorovp ,   * (был изменён)

Нет, хороший разработчик должен делать такой предположение для любого списка, где более двух элементов (правило "раз — два — много")

–1
faiwer ,  

mayorovp, в точку!

+1
vintage ,  

Где же паджинация в списке тегов у поста и в списке ответов на мой комментарий?

0
+1 –1
mayorovp ,  

А я и не говорил, что пагинация обязательна. Я говорил, что надо рассмотреть предположение.


В данном случае, пагинация оказалась не нужна.

+1
vintage ,  

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

0
mayorovp ,  

Скажите, а как бы вы поступили с тэгами?

0
vintage ,  

Да особо никак, а что?

0
mayorovp ,  

"Особо никак" — это как? Вы бы стали добавлять им пагинацию, виртуализовать скролл или использовать ленивую загрузку для списка тэгов?

0
vintage ,  

Я бы оставил дефолтный ленивый рендеринг, пока не потребовалось бы что-то большее.

–1
mayorovp ,  

То есть ваш фреймворк подписывается на событие скролла на абсолютно любом списке?


То есть любой сайт на $mol, где есть хоть один список, будет тормозить?


Кажется, ваш фреймворк еще хуже чем я думал до этого...

+1
vintage ,  

Нет, есть компонент $mol_scroll который реализует скроллящуюся область и провайдит информацию о видимой области в контекст. Все компоненты внутри могут получать эту информацию через контекст и использовать её при рендеринге.


Уже реализовано несколько раскладок:
$mol_list — для вертикальных списков
$mol_row — для горизонтальных с переносом
$mol_grid — для таблиц с фиксированной высотой строк.
$mol_float — плавающий за скроллингом блок


Также любой компонент может дать подсказку касательно своего минимального размера.

–1
mayorovp ,  

Тогда повторяю вопрос.


Будете ли вы использовать $mol_scroll для списка тэгов?

0
vintage ,  

Нет, я буду использовать его для всей страницы.

+3
Xandrmoro ,  
Какие-то ну очень неумные у вас разработчики
–2
vintage ,  

Просто они не телепаты.

0
+1 –1
noodles ,  
Гайз, может зря накинулись, может тормоза при скролле по одной из причин описанных в статье https://blogs.windows.com/msedgedev/2017/03/08/scrolling-on-the-web/?

У меря на планшете скроллинг не тормозит.
0
youlose ,  
А ещё забавный момент с описаниями товаров и картинками, их тупо нет (картинки есть, но не уникальные в детальном просмотре), потому что если их добавить в данные но они и так уже 700кб, а будут несколько мегабайт (да там гзиповано, но практика показывает что есть куча ситуаций где это не работает + с описаниями будет и гзипованный контент весить очень много). А нет их, потому что будет тормозить загрузка, и автор не сможет показать что его технология полезна…

А ещё предлагаю посмотреть на потребление памяти страницы у меня хром на маке показывает 430 мегабайт.
0
youlose ,  
И при скроллинге потребление памяти растёт, а потом не уменьшается (на 40% сожрал гигабайт памяти)…
0
vintage ,  

Всё замечательно освобождается:


image

0
youlose ,  
image
Вот чуть-чуть поскроллил и несколько минут ничего не делал. Табы habrahabr.ru жрут 136 мегабайт.
–1
vintage ,  

Если вы скроллили только вниз, то с чего бы потреблению памяти уменьшаться?

0
vintage ,  

Описания и картинки не уникальные потому, что это просто наколеночный пример, а не реальный интернет магазин. И нет, описания нет смысла грузить для всех товаров сразу — пусть грузится при открытии подробностей о товаре. Причём АПИ модели при этом ни коим образом не изменится.

0
youlose ,   * (был изменён)
Просто чем ближе к реальному магазину вы будете приближаться, тем меньше вам захочется выводить по 9 тысяч товаров на странице. Люди то решают реальные задачи, а не наколеночные примеры пишут.

P.S. Про картинки, например, в магазинах одежды их по 5 в среднем.
0
vintage ,   * (был изменён)

Ага, я-то пороха не нюхал :-)


Какая разница сколько у товара картинок?

0
youlose ,  
В данном примере, добавляя по нескольку картинок к товарам (и не просто файл, а пути к картинкам), я думаю размер файла данных увеличится раза в 2 минимум. И на мобильном устройстве сидеть с пустым экраном и ждать пока по 3Г загрузиться не очень увлекательно. Плюс потребление памяти от страницы ещё возрастёт.
0
vintage ,  

Путь к картинке лучше генерировать по шаблону из её идентификатора.

–1
youlose ,  
Многие фреймворки и CMS диктуют свои условия, например bitrix при аплоаде картинок пережимает и получается что-то вида "/upload/resize_cache/iblock/bcc/300_400_1/bcc730415057733bd87cfcdbae16d8bf.jpg". Или в рельсах: "/assets/next-15ade8f6ef88eea03d43a98fc6fbde8db53be8cbbfba66bc037a0a02d731e93f.png".
(примеры взял с реальных сайтов)
А есть ещё ситуации когда изображения отдаются с отдельного домена или сервера и тогда там ещё схему и хост приходиться добавлять к каждой картинке.

Или ваш фреймворк подходит только для специально написанного бэкенда?
0
+1 –1
vintage ,  

Если вы выбрали бэкенд, который генерирует такие огромные имена и не позволяет настроить правило их формирования, то да, вам придётся их грузить. И нет, не нужно их грузить сразу для всех товаров — лишь по требованию.


Зачем к каждой картинке добавлять одну и ту же схему и хост? Их можно вынести в отдельную константу.

0
northicewind ,  
Проблема react не в VirtualDOM, а в его текущей реализации. Разработчики, естественно, об этом знают и по этой причине переделывают его см react fiber Смотреть за прогрессом можно здесь, а разницу в производительности на примере того же треугольника Серпинского здесь.
Следующая версия 16.0 уже использует его под капотом. Правда пока в режиме совместимости чтоб сильно не ломать.
+1
vintage ,  

Вообще-то я упоминаю fiber в главе про "треугольник серпинского на Реакте" и подчёркиваю, что это не решает проблему, а лишь размазывает её по кадрам приводя к визуальным артефактам. Можете сами попробовать открыть все реализации и подвигать мышью над кружочками. Файбер перестанет обновлять их содержимое, решив, что это "низкоприоритетное изменение", однако в зависимости от бизнес задачи низкоприоритетным может оказаться как-раз изменение размеров треугольника. Короче, fiber — адский костыль, который преподносят как манну небесную. Я пробовал реализовывать размазывание вычислений по кадрам (причём не только рендеринга, а всех вычислений), но из-за подобных визуальных артефактов отказался.

+1
Sirion ,  
Вообще-то описываемого вами поведения не наблюдается. Пример с fiber — единственный, который у меня работает гладко, и я не вижу никаких артефактов. Ваш пример тормозит почти так же, как исходный.
0
vintage ,  

Зная как они реализованы, не представляю, как оно может у вас не проявляться.

0
Sirion ,  
Хотя вообще да, присмотревшись, начал замечать глюки с наведением. Но всё равно версия с файбером у меня самая плавная.
0
youlose ,  
У mol чаще FPS проседает и больше нагрузка на движок JS
image

P.S. на $mol_atom.actualize проседает
0
vintage ,  

А реакт проваливается редко, но на долго. Подозреваю это от сборщика мусора.

0
Druu ,  
У меня под хромом в версии с файбером вообще цифры не тикают почему-то.
0
vintage ,  

У вас слишком слабый комп, поэтому очередь до обновления чисел не доходит никогда. Мы поэтому и отказались от такой логики.

+1
Druu ,  
> У вас слишком слабый комп

разогнанный i7 6700k
это что надо, чтобы оно тикало?
0
vintage ,  

Возможно уменьшить экран.

0
Druu ,   * (был изменён)
Уменьшал экран, менял масштаб, возюкал мышкой — иногда бывает раз-два тикнет и все. Забагованный этот ваш фибер, короче.

ЗЫ: открыл десять вкладок одновременно — так же 60фпс на каждой, но ничего не тикает. Явно не в железе проблема.
+1
Sinatr ,   * (был изменён)
Кому-то сейчас покажется, что я что-то цитирую… ан нет, я копирую стиль в котором написана статья
и он ни капельки не ...

раздражает.
0
vintage ,  

А это не статья, а текстовая расшифровка доклада. Хотите статью про ОРП — вот же она: https://habrahabr.ru/post/317360/

+3
Ronnie_Gardocki ,  
Таб хрома жрет 1гб+ оперативки если доскроллить до конца, кайф.
Вся реализация это грустная история о том, как инженер, не дружащий с UX, запилил решение, избавляющее юзера от проблем, которые на самом деле у него никогда не существовали. А само решение это просто ад для любого юзера, вследствие чего статья кажется нулевой с точки зрения полезности (если даже не вредной для неокрепших умов новичков).
–2
vintage ,  

Не напомните, что там у гугла на 100 странице? То, что вам дали скроллинг на 10000 товаров вовсе не означает, что нужно мотать его до конца. Так же как наличие 100 страниц в выдаче никого не заставляет идти до последней. Если пользователь не нашёл нужного в первой сотне товаров, то скорее всего он воспользуется фильтром и будет прав. Именно этот кейс мы поощряем, показывая ему панель с возможностью конкретизировать выборку.

+1
faiwer ,  

С такой философией тупо лимитируйте выборку. В google зайдя на 100-страницу я не столкнусь в тем, что он отожрал 1 GiB памяти и всё страшно тормозит. Скажем когда я первый раз прикручивал SphinxSearch я лимитировал результаты 100-ей элементов. Вот что угодно можно на странице жать — она не зависнет. Даже если случайно куда-нибудь не туда ткнуть. В вашем случае достаточно просто ползунок дёрнуть. И дёрнут. Это обычный паттерн поведения пользователя ― бегло осмотреть что ему предложено. Сделайте на худой конец тогда уж "overflow-y: hidden", коли уж система столь уязвима.

0
vintage ,  

overflow:hidden вообще запретит прокрутку. Это не решение. Если хотите — можете лимитировать выборку любым числом. Надеюсь не надо показывать как работать с функцией slice?

+1
faiwer ,  
overflow:hidden вообще запретит прокрутку

хоспади, приделайте свой собственный. Всяко лучше, чем сейчас ;)


Надеюсь не надо показывать как работать с функцией slice?

Дык я то умею. Мои интерфейсы не тормозят, ни у 90%, ни у 10%. И током не бьются тоже.

0
vintage ,  

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

0
faiwer ,  
  1. Про отдельный тред вы пишете ввиду того, что заранее ожидаете таких тормозов, что это должно играть значимую роль? :)
  2. Прошлый раз я прицепил отдельный <div/>с нативным скролл-баром, отслеживая в нём onScroll.
0
vintage ,  

Вы не сможете перерендерить весь экран целиком менее чем за 16мс на любом девайсе, когда пользователь дёргает скроллбар.


То есть вы предлагаете отключить нативный скролл, чтобы эмулировать его через нативный скролл? Ммм, хитро.

+2
Druu ,   * (был изменён)
Разница с гуглом кардинальная — если в гугле я хочу (по каким-то причинам, вот хочу) попасть на 100 страницу, то я просто жму на 100 страницу. И все работает. То есть если функционал предоставлен — то нет проблем в его использовании. А что я делаю в вашем случае? Тереблю колесико 5 минут или пользуюсь скролом, который вешает вкладку? Какой смысл выводить потенциальные 9000 товаров, если пользователь все равно не сможет до этой условной девятой тысячи добраться? А если доберется — то у него все будет тормозить, виснуть, жрать память и т.д.? Получается, вы предоставили возможность домотать до 9000 товара, но тут же говорите, что «мотать не надо». А зачем даете потенциальную возможность, если «не надо»?
0
vintage ,  

Потому, что мы не знаем сколько у пользователя памяти и как далеко он хочет домотать. Ну ограничим мы каким-то числом, а пользователю надо на 1 больше. В итоге он не получит своего, хотя разница в 1 погоды не сделает.

+1
Druu ,   * (был изменён)
> Потому, что мы не знаем сколько у пользователя памяти и как далеко он хочет домотать.

Так он все равно не домотает, докуда хочет, зачем предоставлять возможность, которую нельзя использовать? Или вы полагаете, что кто-то правда будет 5 минут крутить?..

Вы знаете, это как на сайт повесить кнопку, при клике на которую она будет вешать вкладку. Можно, конечно. Но зачем?
–1
vintage ,  

А почему вы предполагаете, что он хочет пренепременно в самый конец огромного списка?

0
Druu ,  
Так еще раз, если ему нельзя далеко скроллить, то зачем давать такую возможность? Уберите ее.
Одно дело, если бы оно работало — бог с ним, пусть, но оно ведь тормозит и виснет.
–1
vintage ,  

У пользователя есть возможность запустить 1000 программ, от чего комп тормозит и виснет. Какой лимит на одновременно запущенные программы вы бы установили?

+1
mayorovp ,  

У пользователя нет кнопки "запустить 1000 программ". Чтобы запустить 1000 программ — надо сделать 2000 кликов.


Вы же дали пользователю кнопку, на которую ему нельзя нажимать. Так не делается.

–1
vintage ,  

Программа может быть и одна. Бенчмарк какой-нибудь многопоточный.

0
mayorovp ,  

Нет, мы не предполагаем это. Мы допускаем такую возможность.

–1
vintage ,  

Если пользователю нужно в конец — лучше предложить ему возможность поменять ориентацию сортировки.

+1
mayorovp ,   * (был изменён)

Лучше. Но если вы дали возможность прокрутить в конец списка скролом — вы обязаны предусмотреть что пользователь этой возможностью воспользуется.

–1
vintage ,  

Если у него достаточно ресурсов для этого — пусть действует, какие проблемы?

+2
mayorovp ,  

Если вы считаете, что никто не будет смотреть 100ю страницу — просто не выводите ее. Нет нужды вешать браузер ради мифической "красоты" решения.

0
vintage ,  

А есть смысл пользователю что-то запрещать, если ему очень надо?

+1
mayorovp ,  

Есть смысл делать сайт, который работает.

0
vintage ,  

Не стоит выдавать ограничение некоторых вымышленных сценариев за "ничего не работает". В том же ЯМаркете тоже можно накликать 100500 элементов. И что? А ничего, никто так далеко не листает.

+2
Druu ,  
В вашем случае не надо делать 100500 кликов, надо сделать один (!) клик
0
vintage ,  

Не получится за 1 клик никак. Как минимум — надо хватануть и игнорируя тормоза довести курсор до самого низа.

0
mayorovp ,  

Достаточно хватануть и дернуть мышкой. После этого пользователь уже не сможет вернуть ползунок скрола обратно из-за начавшихся тормозов.

+1
faiwer ,  

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

–1
vintage ,  

Боюсь вы, в данном случае, не репрезентативны, так как ваша цель потестить реализацию, а не найти товар.

+2
raveclassic ,   * (был изменён)
Ой да бросьте. Просто признайте, что не работает. Тут все машинально потянулись к ползунку.

Edit: окей, я по привычке сначала нажатым колесиком попробовал. После того, как не вышло, потянулся к ползунку.
+1
youlose ,  
Я тоже тыкнул, не для проверки релизации, а посмотреть какие другие виды товаров есть и это вполне ожидаемое поведение от пользователя.
0
tenbits ,  

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

+2
youlose ,  
Решения восхищают когда они решают какие-то проблемы лучше других, придуманных ранее. А когда под решение придумывается проблема, это странно.
0
gribnoysup ,  
Если бы автор продавал нам в этом докладе «сферическое ОРП в вакууме», то ок, никаких претензий и вопросов по поводу демо. Но автор пытается продать нам свое решение, а не просто теорию, поэтому демо это важно. Большие виртуальные списки встречаются не так редко как кажется: посмотрите ленту вк, фейсбука, твиттера, или например списки с историями операций в разных банковских онлайн сервисах.

Легко поливать говном react, но списки на mobile.twitter.com не убивают вкладки в браузерах и не тормозят каким бы образом вы их не скроллили. Посмотрите на примеры для библиотеки react-virtualized. Как можно говорить что vdom в react-е работает медленнее, чем моловское обновление DOM, если на примерах видно, что это не так?
0
vintage ,  
Но автор пытается продать нам свое решение, а не просто теорию

Автор продаёт сферический ОРП, а примеры приводит на $mol_mem, так как остальные реализации ОРП поддерживают не все из описанных возможностей ОРП. У меня была мысль сделать некоторые примеры, например, на MobX. Но от этой идеи пришлось отказаться, чтобы не объяснять синтаксис каждой библиотеки, ибо время доклада не резиновое. И так пришлось урезать темы про логику работы ОРП-алгоритмов и про неидемпотентные реактивные задачи.


Посмотрите на примеры для библиотеки react-virtualized.

Вот вам виртуальный скролл на моле: http://mol.js.org/#demo=mol_grid_demo


Я уже устал повторять, что виртуальный скролл имеет свои ограничения, что не позволяет использовать его везде. А ленивый рендеринг является не аналогом виртуального скролла, а аналогом "рендеринга вообще всего за раз".


Как можно говорить что vdom в react-е работает медленнее, чем моловское обновление DOM, если на примерах видно, что это не так?

В докладе я привожу в пример треугольник серпинского, там нет никакого ленивого рендеринга.

+1
gribnoysup ,  
Вот вам виртуальный скролл на моле: http://mol.js.org/#demo=mol_grid_demo

1000x14 и не успевает за скроллом? Спасибо, оставьте себе такие виртуальные гриды


А ленивый рендеринг является не аналогом виртуального скролла, а аналогом "рендеринга вообще всего за раз".

Аналогом рендеринга всего ваше решение не является, оно ломает поиск по странице, как это может быть аналогом рендеринга всего? Ближе всего оно именно к виртуальному скроллу (если вам слово скролл не нравится, зовите это виртуальным рендерингом или как угодно еще)

0
vintage ,  

Не стесняйтесь, покажите пример, который успевает.


Процент пользователей, умеющих в поиск по странице, катастрофически мал. И это без учёта его катастрофической ущербности.

0
amaksr ,  
Либо можно перехватить Ctrl-F, как это делает гугль-док.
0
vintage ,  

Я уменьшил число товаров до 1000 (вся номенклатура средненького магазина) и сделал более редкое обновление числа отзывов. Надеюсь теперь мы сможем обсудить сам доклад, а не только лишь иллюстрацию к нему слепленную за пару вечеров?

0
+1 –1
mikolalex ,  
Нет, никого (из сравшихся) это уже не интересует, т.к. нужно вникать, сравнивать с другими RP реализациями, а они, видимо, дальше мейнстримовых фреймворков в своем развитии пока не дошли…
Автору спасибо за статью, невольно позавидовал вашему легкому слогу. Сейчас работаю над статьей по другой RP-библиотеке, решил добавить в нее(статью) некоторые из ваших примеров, чтобы можно было параллельно сравнить подходы… Так что, полемика будет, но чуть позже.
0
vintage ,  

А вы какую библиотеку используете?

+1
Sirion ,  
Боюсь, поезд уже ушёл. Не бывает второго шанса создать первое впечатление)
+1
+2 –1
babylon ,   * (был изменён)

vintage Вы как обычно воюете со стоячими ветряными мельницами. А они злобно минусуют вам карму. Вы слишком технологичны для того чтобы вас понимал среднестатистический хабражитель. У меня бекграунд флешовый поэтому я вас понимаю уже только потому что на флеше эта (или похожая на неё )тема была обжеванна много раз и много лет тому назад. Хотя кому это может показаться откровением. Но с флешем люди и не так извращались в своё время. Но это время прошло. Начинаем велосипедить по новой.

0
vintage ,   * (был изменён)

А к чему в итоге во флеше пришли? (Помимо того, что сам флеш заглох)

0
babylon ,  

Дмитрий, да кто к чему. Однозначно заполнять компонент невидимыми в скроллируемой области данными не лучшее решение. Лично я дергал такое количество, чтобы всегда кэшировалось число записей равное числу записей видимой области умноженное на 3 (неважно поля или строки). Что касается лейаута. Я строил сразу весь визуальный объект по JSON структуре модели, а рендерил и отображал в соответствии с бизнес логикой активной сцены. Как всегда для меня сложность была с кастомизацией инпутов. Не знаю итог это для кого-то или нет.

0
+1 –1
vintage ,  

Появилось видео с выступления:



Ответы на вопросы с 30 минуты.