В реактивной системе все состояния связаны друг с другом инвариантами в единый граф. Когда мы изменяем что-то с одной стороны этого графа, рантайм обеспечивает каскадный пересчет зависимых состояний. Такие последовательности пересчетов — это ничто иное, как потоки информации (data-flow). Чем прямолинейнее эти потоки, чем меньше они разветвляются и затрагивают состояния, не относящиеся к изменениям, тем эффективнее работает система. И здесь есть два подхода к оптимизации потоков информации…
🦽 Ручной
🚕 Автоматический
🦽Ручное конфигурирование
В библиотеках с push-моделью реактивности сложно автоматизировать потоки, поэтому процветает их ручное управление. Это означает, что мы получаем два типа ошибок…

Во-первых, мы можем забыть подписаться на что-то, что приводит к несогласованности. В примере мы забыли подписаться на Title, и при его изменении Greeting не пересчитывается.
Во-вторых, мы можем забыть отписаться от чего-то, что приводит к ненужным вычислениям. В примере мы забыли отписаться от Name, и при его изменении Greeting пересчитывается, но получает то же значение.
Но если с ошибками еще можно как-то справиться, то со сложностью ручных оптимизаций справиться уже не так просто. Для банального логического ветвления нужно вручную реализовать транзистор, где есть управляющий поток, переключающий выход между двумя входами. Для циклов и косвенной адресации все становится настолько сложным, что мало кто способен адекватно это описать. В итоге получается, что вместо точечных пересчетов многие состояния пересчитываются при любом чихе, что довольно медленно.
Посмотрите на этот FRP-ребус и попробуйте сразу сказать, что он делает и почему:
const ToysSource = new Rx.BehaviorSubject( [] ) const mem = Rx.pipe( distinctUntilChanged(), debounceTime(0), shareReplay(1), ) const Toys = ToysSource.pipe( mem ) const FilterSource = new Rx.BehaviorSubject( toy => toy.count > 0 ) const Filter = FilterSource.pipe( mem ) const ToysFiltered = Filter.pipe( switchMap( filter => { if( !filter ) return Toys return Toys.pipe( map( toys => toys.filter( filter ) ) ) } ), mem, )
А он делает простую вещь: создает поток продуктов, поток критериев фильтрации и из них получает поток отфильтрованного списка продуктов.
Здесь уже применено несколько стандартных оптимизаций. Однако этот код работает не очень эффективно: список игрушек фильтруется заново даже если изменились данные продукта, от которых результат фильтрации не зависит. Чтобы преодолеть эту проблему, придется усложнить код в разы, но мало кто с этим справится.
Этот подход приводит к сложному, трудно поддерживаемому коду. Его сложно читать. Его сложно писать. Его лень писать правильно. И легко ошибиться, если, конечно, вы не финалист специальной олимпиады по информатике.
🚕Автоматическое конфигурирование
Библиотеки с pull-моделью реактивности обычно используют автоматическое отслеживание зависимостей. Это не только гораздо надежнее, но и чрезвычайно просто для прикладного программиста. Ему не нужно думать о потоках данных — они динамически настраиваются рантаймом в наиболее оптимальном (для данного состояния приложения) виде.

Здесь программисты делятся на два лагеря: одни боятся этой «магии», потому что не понимают, как это работает, другим просто плевать — работает и работает, на одну головную боль меньше.
Ну а на обочине стоит лагерь тех, кто просто знает, как это работает, и использует эти знания с пользой. Ведь, как известно: любая достаточно развитая технология неотличима от магии… для непосвященного.
Практика показывает, что (с автоматизацией) код приложения в разы меньше, сам код гораздо проще и надежнее, а приложение работает быстрее.
class $my_toys { @mem toys( next = [] ){ return next } @mem filter( next = toy => toy.count() > 0 ) { return next } @mem toys_filtered() { if( !this.filter() ) return this.toys() return this.toys().filter( this.filter() ) } }
Разве код ОРП мемами из $mol_wire не намного проще и понятнее? Это тот же код, который мы бы написали без реактивного программирования, но мы добавили специальный декоратор, который динамически отслеживает зависимости при их доступе, кеширует результат выполнения функции и сбрасывает кеш при изменении зависимостей.
Правильная реализация логики этих декораторов позволяет выполнять вычисления наиболее оптимально, не перекладывая головную боль управления потоками данных на программиста приложения.
Аналогично можно добавить отсортированный список продуктов, зависящий от функции сортировки и отфильтрованного списка.
@mem sorter( next = ( a, b )=> b.price() - a.price() ) { return next } @mem toys_sorted() { if( !this.sorter() ) return this.toys_filtered() return this.toys_filtered().toSorted( this.sorter() ) }
По умолчанию тут сортировка по цене, значит сортировка будет выполняться заново только если изменится цена любой игрушки из отфильтрованного списка, сам отфильтрованный список или критерий сортировки.
Причём приведённый тут код нисколько не изменится, даже если данные будут грузиться с сервера, но об этом расскажу в другой раз. А пока, подписывайтесь на что-нибудь, вступайте во что-то там, и держите руку на пульсе вот этого вот.
