Идея написания статьи появилась в этой ветке, может кому-то будет интересно и её почитать. Сразу скажу, писатель (в том числе кода) из меня так себе, но я буду стараться.


Писать будем как обычно тудулист, надоел конечно до чёртиков, но что-то лучшее для демонстрации придумать сложно. Сразу ссылка на работающее приложение: жмяк (код).



Данные приложения


И сразу в бой, начнём с хранилища. Единственный тип необходимый для этого приложения — Todo:


import { EventEmitter } from 'cellx';
import { observable } from 'cellx-decorators';

export default class Todo extends EventEmitter {
    @observable text = void 0;
    @observable done = void 0;

    constructor(text, done = false) {
        super();

        this.text = text;
        this.done = done;
    }
}

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


Наследование от cellx.EventEmitter необходимо на случай если в дальнейшем понадобится подписаться на изменения какого-то поля:


todo.on('change:text', () => {/* ... */});

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


Теперь напишем корневое хранилище:


import { EventEmitter, cellx } from 'cellx';
import { observable, computed } from 'cellx-decorators';
import Todo from './types/Todo';

class Store extends EventEmitter {
    @observable todos = cellx.list([
        new Todo('Primum', true),
        new Todo('Secundo'),
        new Todo('Tertium')
    ]);

    @computed doneTodos = function() {
        return this.todos.filter(todo => todo.done);
    };
}

export default new Store();

Здесь уже поинтереснее. Используется cellx.list (алиас для new cellx.ObservableList) — наблюдаемый список, наследует от cellx.EventEmitter и при любом своём изменении генерирует событие change. Наблюдаемое поле получая в качестве значения что-то наследующее от cellx.EventEmitter подписывается на его change и тоже изменяется при этом событии. Всё это значит, что не обязательно использовать встроенные коллекции, можно сделать свои унаследовав их от cellx.EventEmitter. Из коробки есть cellx.list и cellx.map. Отдельным модулем есть индексируемые версии обоих коллекций: cellx-indexed-collections.


Ещё один новенький — декоратор computed, вычисляемые поля — это самая суть cellx-a — вы просто пишите формулу вычисляемого поля, вам не нужно самому подписываться на done каждого todo при его добавлении и отписываться от него же при удалении, всё это делает cellx пока вы не видите, вам остаётся расслабиться и получать удовольствие описывая самую суть. При этом описание происходит, можно сказать, в декларативном виде — уже не нужно думать о событиях и о том как изменения будут распространяться по системе, всё пишется так, как будто отработает лишь раз. Кроме того cellx очень умный и автоматически делает некоторые хитрые оптимизации: динамическая актуализация зависимостей и схлопывание и отбрасывание событий не допустят избыточных расчётов и обновлений интерфейса. Если делать всё это вручную, код получается довольно объёмным, но, что намного хуже — глючным. Отладкой же cellx-а заниматься приходиться раз в сто лет, он просто работает.


Представление приложения


Переходим к слою отображения. Сначала компонент задачи:


import { observer } from 'cellx-react';
import React from 'react';
import toggleTodo from '../../actions/toggleTodo';
import removeTodo from '../../actions/removeTodo';

@observer
export default class TodoView extends React.Component {
    render() {
        let todo = this.props.todo;

        return (<li>
            <input type="checkbox" checked={ todo.done } onChange={ this.onCbDoneChange.bind(this) } />
            <span>{ todo.text }</span>
            <button onClick={ this.onBtnRemoveClick.bind(this) }>remove</button>
        </li>);
    }

    onCbDoneChange() {
        toggleTodo(this.props.todo);
    }

    onBtnRemoveClick() {
        removeTodo(this.props.todo);
    }
}

Здесь из новенького — декоратор observer из модуля cellx-react. Грубо говоря, он просто делает метод render вычисляемой ячейкой и вызывает React.Component#forceUpdate при её изменении.


Остаётся корневой компонент приложения:


import { computed } from 'cellx-decorators';
import { observer } from 'cellx-react';
import React from 'react';
import store from '../../store';
import addTodo from '../../actions/addTodo';
import TodoView from '../TodoView';

@observer
export default class TodoApp extends React.Component {
    @computed nextNumber = function() {
        return store.todos.length + 1;
    };

    @computed leftCount = function() {
        return store.todos.length - store.doneTodos.length;
    };

    render() {
        return (<div>
            <form onSubmit={ this.onNewTodoFormSubmit.bind(this) }>
                <input ref={ input => this.newTodoInput = input } />
                <button type="submit">Add #{ this.nextNumber }</button>
            </form>
            <div>
                All: { store.todos.length },
                Done: { store.doneTodos.length },
                Left: { this.leftCount }
            </div>
            <ul>{
                store.todos.map(todo => <TodoView key={ todo.text } todo={ todo } />)
            }</ul>
        </div>);
    }

    onNewTodoFormSubmit(evt) {
        evt.preventDefault();

        let newTodoInput = this.newTodoInput;

        addTodo(newTodoInput.value);

        newTodoInput.value = '';
        newTodoInput.focus();
    }
}

Здесь ещё парочка вычисляемых полей, отличаются от Store#doneTodos они лишь тем, что поля из которых они вычисляются лежат не на текущем экземпляре (this), а где-то в другом месте, cellx никак не ограничивает в этом плане, эти поля можно спокойно переместить в Store и всё так же будет работать. Определять, где должно лежать поле лучше по его сути — если поле специфично для какого-то определённого компонента, то пусть в нём и вычисляется, светиться в общем хранилище ему нет смысла. В данном случае я бы #leftCount перенёс в хранилище, оно вполне может пригодиться где-то ещё, а #nextNumber вполне неплохо смотриться и здесь.


Бизнес-логика приложения


В экшенах cellx никак не используется, поэтому я максимально их упростил, получился даже не Flux, а какой-то MVC в терминах Flux-а. Надеюсь вы мне простите это упрощение.


Результат


В данном случае приложение совсем простое и написать его так же просто можно и без cellx-а (никаких подписок на каждый done здесь не потребуется), при дальнейшем же усложнении связей в приложении сложность их описания на cellx-e растёт линейно, без него — обычно нет и в какой-то момент приходим к мешанине событий в которой без поллитра не разобраться. Для решения проблемы, кроме реактивного программирования, есть и другие подходы со своими плюсами и минусами, но их сравнение — уже другая история (если кратко, как минимум они проигрывают из-за большого количества лишних вычислений и, как результат, более низкой производительности).


В общем-то по коду это всё, ещё раз ссылка на результат: жмяк (код).


Сравнение с другими библиотеками


MobX


Чаще всего спрашивают отличия от MobX. Это наиболее близкий аналог и отличий немного:


  1. cellx примерно в 10 раз быстрее.
  2. В статье про атомы я подсмотрел методы/опции put и pull, позволяющие ячейкам уметь чуть больше: синхронизация значения с синхронным хранилищем, синхронизация значения с асинхронным хранилищем, про pull. У MobX я ничего похожего не нашёл.
  3. Разная система очистки памяти, у cellx это пассивный режим, в MobX вообще нельзя отписаться от ячейки после подписки, что для меня какая-то странность, когда необходима отписка необходимо использовать autorun, который можно "убить" возвращаемым disposer-ом. Из минусов autorun-а — инициализирующий запуск колбека часто вообще не в тему.
  4. MobX лучше интегрирован с React-ом, в отличии от cellx-а он так же как-то вклинивается в слой бизнес-логики приложения. Я так и не понял зачем он там, но видимо зачем-то нужен.
  5. У MobX явно лучше с документацией.

Kefir.js, Bacon.js


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


var val2 = cellx(() => val() + 1);

На этих библиотеках превратиться в что-то вроде (псевдокод, как там точно я не помню, да и не суть):


var val2 = val.lift(add(1));

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


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


Подвал


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


ЗЫ: Кстати, мы ищем разработчика в команду — вакансия.

Поделиться с друзьями
-->

Комментарии (81)


  1. xGromMx
    18.10.2016 20:04
    +2

    Сравни свой cellx со скоростью https://github.com/cujojs/most


  1. RubaXa
    18.10.2016 23:08
    +1

    А теперь представим, что React, в следующей версии меняет логику вызова render и начинает вызывать render вложенных блоков в рамках render'а родительского, а не последовательно, как сейчас. Что будете делать в этом случае? Если я правильно понял, то в этом случае любое изменение приведёт к перерендеру всего App, а cellx тупо станет лишним оверхедом.


    1. vintage
      19.10.2016 01:00
      +1

      При использовании OORP сам реактовский рендерер становится оверхедом.


      1. RubaXa
        19.10.2016 09:16

        Эта статья про «React + ___», так что правильный ответ: OORP (даже не знаю что это) является оверхедом при использовании с React.


        1. vintage
          19.10.2016 09:33

          Объектно Ориентированное Реактивное Программирование. То, что реализует cellx.


          1. babylon
            23.10.2016 16:54

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


            1. vintage
              23.10.2016 17:43

              В том, что "реакции" (те самые реактивные переменные) не в воздухе висят, а принадлежат объектам. И изменение состояния одного объекта реактивно отражается на других объектах. В том числе приводит к созданию и уничтожению других объектов обладающих реакциями.


              1. babylon
                24.10.2016 17:09

                Это уже зависимости. У меня некоторое де-жа-вю. Как будто ты на флеш форуме в 2008г. Ничего не меняется в датском королевстве.


  1. indestructable
    19.10.2016 00:16
    +5

    Основная идея Реакта — это быстрое обновление представления по новым (независимым от существующих) данным. В идеале — берем данные, рендерим вью, меняем данные (как угодно), даем вью — получаем оптимальное (ну или хотя бы достаточно быстрое) обновление представления.


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


    Но при этом всем он вообще не требует вью для своей работы. Он не требует библиотек, наследования, использования своих сервисов или своей реализаций коллекций. И это прекрасно, а вдвойне прекрасно, что их там некуда впихивать (иначе кто-нибудь их туда точно впихнул бы).


    Что больше всего раздражало в Нокауте: конвертация и расширение загруженного JSON-a в Observable. Иначе не работали даже примитивные формы редактирования. Это проникновение библиотеки на уровень, где ее не должно быть, и без нее можно обойтись, когда постоянно нужно помнить, что у тебя — данные, а что — модели, и почему же, все-таки, они не могут быть одним и тем же.


    1. Strate
      19.10.2016 11:29

      Идея то идеей, однако в большом развесистом приложении со сложными взаимосвязями рано или поздно всё придёт к тому, что на каждый чих будет вызываться render каждого компонента. А их много. Причём в резальтате ререндера не происходит обновления DOM — то есть ререндер был вхолостую. mobx/cellx позволяют эту порблему решить точечно дёргая рендер только тех компонентов, которые действительно надо обновить.
      Да, в redux того же можно достичь, но с большим объёмом ручной работы, с использованием всяких reselect, с хитрыми shouldComponentUpdate и так далее.


      1. RubaXa
        19.10.2016 11:58
        +2

        По моему это само обман, добавить shouldComponentUpdate равносилен прописыванию всех этих @observer, @computed и т.п. Притом ещё в подавляющем случае, можно просто сделать декоратор, который будет проверять изменения всех props.


        P.S. Не стоит записывать меня в защитники React, редко использую, но смотря на примеры с реактивностью и «перерендер на каждый чих», я выбираю второе.


        1. lega
          19.10.2016 12:27

          Притом ещё в подавляющем случае, можно просто сделать декоратор, который будет проверять изменения всех props.
          Дак это же diry-checking (тот самый что в angular.js), только в итоге будет 2 dirty-checking'a, один на данные, другой на vdom.


          1. RubaXa
            19.10.2016 12:41

            Воу-воу, при сравнении vdom нет dirty-checking, там строгая проверка, фиксированного списка свойств, в отличии от ангуляра с его ограниченным (если не ошибаюсь) deepEqual. Во-вторых, вы так пишите, как будто, это что-то плохое, взять и проверить фиксированный набор свойств, которые описаны в propTypes.


            1. Strate
              19.10.2016 12:50

              React не проверяет propTypes при сравнении vdom.


              1. RubaXa
                19.10.2016 13:04

                Вы точно меня по диагонали читаете ;]


            1. lega
              19.10.2016 13:03
              +1

              при сравнении vdom нет dirty-checking, там строгая проверка, фиксированного списка свойств, в отличии от ангуляра с его ограниченным (если не ошибаюсь) deepEqual.
              DeepEqual там не часто, обычно идет проверка списка свойств. В vdom хоть и узкий список свойств, но проверять приходиться все* дерево, что и делает его dirty-checking.

              Во-вторых, вы так пишите, как будто, это что-то плохое
              Нет, я за, он работает быстро.


        1. Strate
          19.10.2016 12:50

          Нет не равносилен, потому что shouldComponentUpdate блочит ререндер всего поддерева компонента, а mobx/react позволяет доставлять точечно обновления куда следует.


          https://twitter.com/mweststrate/status/718444275239882753 — тут можно посмотреть сравнение туду-листа написанного с использованием redux vs mobx. С тем же mobx приходится писать сильно меньше обслуживающего кода, и получаем из коробки производительность.


          https://habrahabr.ru/post/304340/ — здесь отличный пример того, о чём я говорю. Посмотрите в статье бонусный совет в самом конце.


          1. RubaXa
            19.10.2016 12:54

            Вы меня не поняли, это ответ на:


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

            Я к тому, что определить shouldComponentUpdate не сложнее, чем расставить все эти @observer и т.д.


            1. Strate
              19.10.2016 12:55

              Сложнее. Почитайте https://habrahabr.ru/post/304340/


          1. RubaXa
            19.10.2016 13:02
            +1

            Теперь про бенчмарк и mobx.


            Вы смотрели его исходники? Заметили, как именно пришлось написать ему приложение, чтобы всё раком не встало. В итоге компоненты, которые должны просто получать «голые» данные и быть независимыми и универсальными, передают от компонента к компоненту ссылку на store. И такое происходит на каждом уровне, он даже в TodoItem передаёт не id, title, completed или просто <TodoItem {...todo}/>, а именно todo={todo}.


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


            1. Strate
              19.10.2016 15:09

              Проброска в компоненты store — всего лишь один из вариантов написания приложения. Вы можете пробрасывать либо через контексты (как и делает react-redux в его функции connect), либо вы можете оставить store только в рутовом компоненте, но пробросить во внутренние компоненты чистые данные/коллбеки и всё будет работать.


              1. RubaXa
                19.10.2016 15:29

                Проброска в компоненты store — всего лишь один из вариантов написания приложения

                Это костыль, правильно, это в app собрать данные, и распихать их по компонентам через props. Но в примере mobx, абсолютно все компоненты завязаны на конкретный store с его конкретными свойствами, да так, что реюзабельность компонентов стремиться к 0.


                оставить store только в рутовом компоненте, но пробросить во внутренние компоненты чистые данные/коллбеки и всё будет работать.

                Вот как только вы это сделаете всё ваше приложение замкнется на самой высшей точке App.render и любое изменение будет приводить к его перерендеру. Именно поэтому, автор этого «бенчмарка» так старательно избегает обращения к getter'ам store.


                Кроме этого, в этом коменте я уже писал, что стоит React'у поменять логику вызова render, так даже передача store не поможет.


                1. Strate
                  19.10.2016 15:39

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

                  Именно так и происходит при использовании redux. С mobx — нет. Потому что в нём не меняется store. В нём меняются объекты внтури него. Например при изменении поля name у объекта класса Todo обновится только один конкретный TodoItem, который его отображает. Все соседние не будут изменены, а также сам список тоже. Это будет верно даже если в TodoItem не будет передан (в явном или неявном виде) store. Как раз за счёт реактивности. Должно быть только одно — компонент его отображающий должен быть @observer и всё.


                  Если вам интересно, я могу как нибудь сделать форк mobx-todomvc и переписать его убрав передачу store во все дочерние компоненты, вы увидите что это ничего не изменит.


                  1. RubaXa
                    19.10.2016 16:34
                    +1

                    Давайте я за вас сделаю:



                    Но допустим, что с <TodoItem todo={todo}/> можно смириться и даже убедить себя, что так даже лучше, но что делать, когда нужно сделать динамический текст в кнопке (Add #{XXX})?


                    Так не выйдет: http://codepen.io/RubaXa/pen/EgOPJW?editors=0010?
                    Придется сделать


                    @observer
                    const ButtonTodoAdd = ({todos}) => <button>Add #{todos.length + 1}</button>

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


                    1. Strate
                      19.10.2016 16:49

                      То есть по вашему, необходимость передавать <TodoItem todo={todo}> — это слишком "ненормально"? Вкусовщина чистой воды, мне вот больше нравится передавать сущности отдельным атрибутом, а не spread-оператором.


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


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


                      1. RubaXa
                        19.10.2016 17:03

                        Обратите внимание, что все ваши примеры работают из коробки, ни с чем бороться не приходится

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


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

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


                        Представьте простой компонент Badge, который выводит число, вот чтобы использовать его в mobx, мне придется обернуть его:
                        const BadgeTodos = ({todos}) => <Badge value={todos.length}/>


                        и так далее:
                        const BadgeFooWithBar = ({foo, bar}) => <Badge value={foo.length + bar.length}/>


                        Вот я и пишу дополнительный код, который нужен только для Mobx.


                        1. Strate
                          19.10.2016 17:51

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


                          1. RubaXa
                            19.10.2016 20:46

                            Ох, ну ладно, если вам всё нравится, то кто я такой, чтобы говорить вам обратное, так что всего доброго и хорошего настроения ;]


                          1. farwayer
                            20.10.2016 00:18

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

                            Не, можно еще костыли писать, как RubaXa предложил — такой немного уродливый аналог selector'ов из redux.


                            1. RubaXa
                              20.10.2016 08:33
                              +1

                              Костыли? Теперь так называют официальные рекомендации по оптимизации? Или вы про декоратор? Так это просто ответ на все эти @computed.


                              лишние рендеры отрабатывают

                              Тут такое дело, что создатели реакта много раз уже говорили, что это не проблема, это by design и им всё нравится, он решает ИХ задачи.


                              1. farwayer
                                20.10.2016 11:09
                                +1

                                Если назвать костыли «официальными рекомендациями» — меньшими костылями они от этого не становятся. Или вы не считаете BadgeTodos костылем, который используется только для того, чтобы улучшить производительность? Вместо того, чтобы использовать Badge с числом, приходится писать wrapper.

                                То есть при использовании mobx выбор такой:
                                1. Забить на переиспользуемость компонентов и делать TodoItem, вместо ItemWithTitleAndCompleted (суть, что передаются title и completed, а не todo)
                                2. Забить на лишние рендеры (рендер родительского при изменении только title)
                                3. Писать wrapper'ы, которые обворачивают простые компонеты (TodoItem -> ItemWithTitleAndCompleted). имхо костыль.

                                >> Тут такое дело, что создатели реакта много раз уже говорили, что это не проблема

                                Это не проблема, когда пару десятков компонентов. А вот когда их сотни и больше — уже не круто. Выполнение функций рендера и сравнение virtual dom тоже не бесплатно, поэтому делать это на каждый чих неправильно. Иначе shouldComponentUpdate не существовало бы.


                                1. RubaXa
                                  20.10.2016 11:23

                                  А вот когда их сотни и больше — уже не круто.

                                  Значит вам не подходит React, вот и всё. Я вот не использую его именно по этой причине и mobx никак не спасет, если для вашего проекта типовая операция это полное обновление сразу более 1К элементов, даже наоборот, только мешать будет.


                                  Выбирайте инструмент под задачу, пишите инструмент под задачу, а не как в приведенной статья про react-konva, в которой человек на ровном месте создал себе проблему и героически решал её, а мог бы взять чистую konva и всё, без Redux, Mobx и т.п. Я не понимаю, как можно было сравнивать Redux и Mobx, Redux же не про рендер, он про dataflow и инструментарий вокруг него. Render — это React.


                                  1. farwayer
                                    20.10.2016 11:55

                                    >> если для вашего проекта типовая операция это полное обновление сразу более 1К элементов

                                    Так в том и дело, что обновляется всего один элемент. mobx как раз и заточен на то, чтобы сделать точечный рендер только тех элементов, которые зависят от измененных данных.


                                    1. RubaXa
                                      20.10.2016 12:02

                                      «Полно обновление», это когда один набор даных меняется на другой, понимаете? Тот же пример с todos на mobx, там не просто так акцентируют внимание на изменении todo, но обходят стороной фильтр по completed, потому что он приводит к перендеру всего списка.


                            1. Strate
                              20.10.2016 11:01

                              С селекторами в redux получается точно такой же непереиспользуемый код, но вместо простой обёртки (если она действительно нужна), приходится подключать компонент к стору и писать селекторы.


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


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


                              1. farwayer
                                20.10.2016 11:36

                                В redux это выглядит более естественно: есть стор — достаем из него нужные данные.

                                В mobx: есть данные, но напрямую мы их передавать в компонент не будем — нужна обертка, чтобы dereference случился позже и было оптимально по производительности.

                                mobx (и cellx, конечно) хороший инструмент, но эта его особенность мне не нравится. И это скорее всего не поправишь никак.


    1. Riim
      19.10.2016 15:38
      +2

      Что больше всего раздражало в Нокауте: конвертация и расширение загруженного JSON-a в Observable. Иначе не работали даже примитивные формы редактирования. Это проникновение библиотеки на уровень, где ее не должно быть, и без нее можно обойтись, когда постоянно нужно помнить, что у тебя — данные, а что — модели, и почему же, все-таки, они не могут быть одним и тем же.

      Да! Полностью согласен. Проблема действительно есть, детально её можно разложить по следующим пунктам (первый малость не в тему, но он тоже нужен):


      1) Ренейминг. Сервер присылает fname и lname, а я хочу нормальные firstName и lastName, я не хочу подстраиваться под правила нейминга на сервере и я не хочу на каждый чих идти и договариваться с кем-то из беков, пусть даже они сразу идут навстречу.


      2) Вот пришли данные пользователя и есть класс пользователя (с кучей реактивных полей), я создаю его экземпляр и нужно переписать данные в него. Если считать, что первый пункт как-то решён, то первая мысль, написать функцию setData:


      function setData(instance, data) {
          for (let name in data) {
              instance[name] = data[name];
          }
      }

      Но не всё так просто, далеко не всегда данные в полях просто числа и строки, там часто лежат экземпляры других типов. И вот красивые вызовы типа setData(user, usedData) начинают превращяться в какой-то такой ужас:


      setData(user, {
          ...userData,
          father: setData(father, userData.father)
      });

      Причём всё это происходит постоянно и в заметно большем колличестве.


      3) Нужно постоянно конвертировать массивы и хеши в их наблюдаемые аналоги (cellx.list и cellx.map). Этот пункт похож на предыдущий, их можно слить в один и общий пример получается примерно таким:


      setData(user, {
          ...userData,
          friends: cellx.list(userData.friends.map(friendData => setData(new User(), friendData)))
      });

      4) Почему в предыдущем пункте мы просто создаём новые экземпляры друзей? Что если данные какого-то друга уже запрашивались, где-то используются (есть экземпляр этого пользователя) и новые данные нужно слить в уже существующий экземпляр? Например, есть основные данные пользователя для его маленькой карточки, а остальные данные запрашиваются отдельно при заходе на его анкету. Что-то подобное происходит постоянно и для многих типов приложения где-то в хранилище приходиться заводить хешики в которые по айдишникам складывать существующие экземпляры, а потом постоянно писать так:


      setData(store.userMap[userData.id] || (store.userMap[userData.id] = new User()), userData);

      Это тоже бесит.


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


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


      Теперь про решение, да, редакс с его чистыми данными по большей части не требует никаких решений и в этом его плюс, но он же заставляет отказаться от плюшек реактивного программирования. Если перечисленные проблемы реально автоматизировать до уровня когда они не будут напрягать, то почему бы так и не сделать, сохранив при этом возможность использовать РП.
      Я просто расскажу как всё устроено в текущем проекте, это уже не первая моя попытка всё это решить и пока похоже наиболее удачная.
      По пунктам, первый пункт вполне актуален и при использовании редакса, и сразу вопрос: почему фиксить нейминг нужно именно при конвертировании данных в типы или, в случае редакса, при их записи в хранилище? В любом (или почти любом) приложении есть слой доступа к удалённым данным, он обычно не обсуждается при обсуждении архитектуры, но он есть, у меня он называется proxy (чаще называют providers или как-то так), почему бы не фиксить нейминг в нём? Он лучше для этого подходит, данные он при этом будет возвращать по прежнему чистые, никаких типов, кроме того, сразу после этого слоя можно вообще забыть о существовании какого-то другого нейминга. У меня в папке src/proxy лежит папка converters в которой файлики типа convertUserData.js, через которые пропускаются успешные ответы сервера. Типичное содержимое:


      // @flow
      
      export default function convertContactMediaCompanyData(data: {
          ID: string,
          format: { name: ?string },
          name: ?Array<string>
      }) {
          return {
              id: data.ID,
              formatName: data.format.name,
              names: data.name && data.name.length ? data.name : null // Массив имён, на сервере не любят добавлять `s` в конце имён коллекций
          };
      }

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


      tags: data.tag && data.tag.length ? data.tag.map(convertContactTagData) : null,

      Что бы не дублировать типизацию в текущем конвертере поле tags можно описать как Array<Object>, а уже в convertContactTagData расписать подробнее.
      При таком подходе удаётся убить сразу двух зайцев: пофиксить нейминг и, используя модуль babel-plugin-tcomb сразу провалидировать ответ сервера, тайпскрипт тут не подойдёт, он верит программисту, а при работе с сервером программист не может верить сам себе, tcomb же подставляет реалтаймовые проверки. Это позволяет отлавливать косяки данных сервера не где-то в слое представления и пол дня тратить на понимание причины, а в самом их корне. Конечно же сборка настраивается так, что реалтаймовые проверки подставляются только для dev-сборки (иначе продуктовый бандл сильно раздувается, а для dev-версии это норм и этого достаточно для получения нужного результата).


      Дальше следующие три пункта я решаю модулями @riim/statex и @riim/typify-data. Я не планировал их в ближайшее время кому-то показывать и там нет описания, поэтому опишу здесь. По сути это очень продвинутая версия setData.
      new StateX({}) создаёт корневое хранилище, первый аргумент — карта имён на классы типов, на примере будет понятнее:


      import StateX from '@riim/statex';
      import App from './types/App';
      import User from './types/User';
      // ...
      
      let state = window._state = new StateX({
          App,
          User,
          // ...
      });
      
      export default state;

      Зачем карта будет понятно чуть позже. Дальше метод push — принимает чистые данные (ну почти, typifyData их чуть-чуть портит) и обходя их рекурсивно решает пункты 2-4:


      • массивы оборачивает в cellx.list
      • в объектах ищет поле __type, если нет, то оборачивает в cellx.map, если есть, находит в карте переданной при вызове StateX соответствующий данным класс. Хешики из пункта 4 строятся всегда автоматически для каждого типа: смотрит поле id, ищет его в хешике, при отсутствии создаёт инстанс и запоминает там.
      • если id нет, значит этот тип единичный в приложении, скажем это тип описывающий раздел приложения "Настройки".

      Метод get — возвращает инстанс по типу и, если есть, айдишнику:


      this.user = state.get('User', this.props.userId);

      id приходит с сервера, теперь остаётся только добавить __type. Добавлять его в конвертерах мне кажется не совсем хорошо, proxy должен возвращять чистые данные, а __type — это метаинформация в данных о том, что это за данные. Добавлять их как-то так:


      store.push({
          __type: 'User',
          ...userData
      });

      тоже не хочется, при большей вложенности будет неудобно:


      store.push({
          __type: 'User',
          ...userData,
          friends: userData.friend.map(friendData => {
              __type: 'User',
              ...friendData
          })
      });

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


      store.push(typifyData(userData, { '': 'User', 'friends': 'User' }));

      "Проваливание" в массивы происходит автоматически и как-то отдельно в ключе передаваемого объекта не описывается.
      Можно ещё посложнее:


      store.push(typifyData(userData, {
          '': 'User',
          'tags': 'UserTag',
          'friends': 'User',
          'friends.tags': 'UserTag'
      }));

      Ну и последний пункт. Опять же первая мысль, придумать ещё что-то типа typifyData добавляющее метаинформацию к данным о том как их использовать, а #push научить этому. Плохо то, что вариантов использования очень много, всему #push не научишь, тут можно решить намного красивее и универсальнее. В статье где сравнивается cellx-vs-mobx есть про опцию put, вот она то и нужна, просто покажу как можно решить с бесконечной прокруткой:


      @observable contacts = void 0;
      @observable total = void 0;
      
      @observable({
          put(value, push) {
              if (value) {
                  if (this.contacts) {
                      this.contacts.addRange(value);
                  } else {
                      this.contacts = value;
                  }
              }
      
              push(value);
          }
      }) nextContacts = void 0;

      То есть на уровне конвертеров переименовываем contacts в какой-нибудь nextContacts, в классе же поле nextContacts сохраняет их себе (push(value)), но перед этим дописывает в соседний contacts. Тут можно и без observable свойства, nextContacts может быть обычным сеттером, но тогда от него нельзя будет вычислять другие ячейки, мало ли зачем такое понадобиться. Подобно можно любым образом нестандартно записывать что-то в поле через StateX#push.


      Вот как-то так я и упарываюсь :). Знаю, что это Хабр и сейчас набежит куча экспертов, которые разнесут всё в пух и прах и смешают с авном, не стоило это писать, я просто вижу как это работает, кода получается явно меньше чем даже с редаксом, нет уже этих многоуровневых { ...data, prop: { ...subdata, prop: { ...subsubdata, ... } } } и вся эта ранее постоянно напрягающая конвертация данных уже вообще не заметна, плюс плющки РП и хорошая производительность.


      1. inoyakaigor
        19.10.2016 18:23
        +3

        Я думаю, этот комментарий вполне можно вывести в отдельную статью


      1. indestructable
        20.10.2016 01:16

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


  1. RubaXa
    20.10.2016 13:09
    +3

    @Strate Смотрите, я сейчас специально ещё раз посмотрел на пример в статье и заметил типичную реактивную проблему, а именно вместо уменьшения количества рендеров, мы получаем лишние. Для визуализации проблемы, достаточно добавить: console.log('TodoApp#render') и console.log('TodoItem#render')


    Запустив пример, вы увидите:


    • TodoApp#render
    • (3) TodoItem#render

    А теперь нажмите на чекед:


    • TodoItem#render
    • TodoApp#render
    • (3) TodoItem#render

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


    Всё дело в key, как только title изменится у N-элементов, мы опять получим N лишних рендеров, а потом финальный перендер всего списка. Конечно можно завести поле id, а лучше всего ещё его сделать не observeble, только так можно решить проблему окончательно.


    Я не эксперт, просто задаю вопросы смотря на подобный код, так что очень хотелось бы услышать об опыте Riim, часто ли бывают такие ситуации и что с ними делать, ну или best practices чтобы не стрелять себе в колено?!


    P.S. Ещё бы хорошо получить ответ на мой первый комментарий.


    1. Riim
      20.10.2016 14:03

      услышать об опыте Riim

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


      создатели реакта много раз уже говорили, что это не проблема, это by design и им всё нравится

      не правда, им это нифига не нравиться, они просто не могут с этим что-то сделать, так как там реально тупик, что только я не пробовал, в том числе вообще уходить от виртуальной составляющей, продублирую здесь свой другой комментарий:
      ----
      Плюс у меня есть свои реализации как VDOM, так и патчера без V составляющей. Мне тоже вся эта идея изначально очень нравилась и нахватавшись всяких кейсов вроде описанного выше я попытался исправить их в своём варианте, поняв, что это нормально сделать не получается, начал пилить morph-element, идея была такой: сравнение строк — идеально быстрая операция, причём не зависит от длинны строки, даже при очень длинных и почти совсем одинаковых строках сравнение происходит сверхбыстро где бы не находилось различие. Дальше вместо подстановки разметки дочернего компонента за счёт вызова его как функции, родитель должен был генерировать кастомные элементы разметка которых разворачивалась уже в attachedCallback (ныне connectedCallback) (он синхронный, полифилится на MutationObserver не синхронно, но это тоже удалось решить). Что это давало? Получалось, что каждый компонент генерирует только свой собственный контент, но не контент потомков, то есть при первой генерации можно просто запомнить эту строку и при последующей сравнивать не два огромных объекта со всем контентом потомков, а две относительно короткие строки, что в 100500 раз быстрее. Ну и естественно не перегенерировать контент потомков при равности контента родителя с предыдущим вариантом. То есть лишняя перегенерация всё же происходила (родитель), но уходила не на всю глубину, а максимум на один уровень. Такой подход решал некоторые проблемы VDOM, но не все и в целом патчинг с реального DOM (без виртуальной составляющей) всё же получается медленней, как бы я не пытался его оптимизировать и что бы там не говорил разработчик оригинального morphdom.
      В общем, пока я здесь в тупике, а все другие варианты, что я вижу, предлагают костыли вроде:


      добавляем dirty флаг

      используем внутренние часики и проверяем по дате модификации

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


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


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

      В этом случае сломается не только cellx-react, многое должно сломаться и вряд ли они будут так круто что-то менять.


      1. poxu
        20.10.2016 16:47

        Сейчас я для себя пришёл к выводу, что VDOM — тупиковая идея

        Скажите пожалуйста человеку плохо понимающему в вопросе. VDOM это то на чём построен реакт и оба ангуляра?


        1. Riim
          20.10.2016 17:12

          Только реакт.


  1. vintage
    20.10.2016 13:55

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

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


    1. RubaXa
      20.10.2016 15:09

      Попробую псевдокод написать:


      items = [{value: 'foo'}]
      
      rootLevel()
        var x = items.length ? items[0].value : 0;
        console.log('root:', x)
        items.forEach(item => subLevel(x, item))
      
      subLevel(x, item)
        console.log(`item: ${x} + ${item.value}`)
      
      rootLevel()
      // root: foo
      // item: foo + foo
      
      items[0].value = 'bar'
      // item: foo + bar
      // root: foo
      // item: bar + bar

      Где и почему я ошибся?


      1. Riim
        20.10.2016 16:51

        http://jsbin.com/vexugulala/1/edit?js,console


        // root: foo
        // item: foo + foo
        // root: bar
        // item: bar + bar


        1. RubaXa
          20.10.2016 17:35

          Эээ, нееее, так не пойдет, subLevel же тоже должен быть реактивным :]


          1. Riim
            20.10.2016 17:43

            1. RubaXa
              20.10.2016 17:54

              Давайте попробую текстом описать. Смотрите, есть два уровня: root, в нем sub, root принимает массив элементов и итерируется по нему вызывая sum, которому передает value первого элемента массива и сам элемент. Дальше я ожидаю, что при изменение первого элемент, произойдет обновление начиная с root, но если обновить любой другой, то должен обновиться только связанный sub, возможно такое?


              1. Riim
                20.10.2016 17:59

                Ага, понял, в прошлый раз не правильно написал, вот так должно быть: http://jsbin.com/fuhepiboci/edit?js,console
                Вывод:


                root: foo
                item: foo + foo
                item: foo + bar
                root: bar
                item: bar + bar

                Первые три — инициализация, остальное, вроде то, что ожидается)


                1. Riim
                  20.10.2016 18:01

                  Блин, root: bar лишний))


                1. RubaXa
                  20.10.2016 18:08

                  Вот теперь правильно и проблема на лицо: http://jsbin.com/pixuqijinu/1/edit?js,console


                   ~~~ START ~~~
                  root: foo
                  item: foo + foo
                  ~~~ CHANGE ~~~
                  item: foo + baz
                  root: baz
                  item: baz + baz


                  1. Riim
                    20.10.2016 18:21

                    На самом деле всё правильно, rootLevel читает items[0].value и следовательно подписывается на него, если всё же нужно неправильно, то нужно как-то прочитать его без автоматически срабатывающей при этом подписки. Раньше для этого можно было при чтении передать false, потом я убрал эту возможность, вроде нигде не пригождалась, поэтому сейчас только читать скрытое поле: http://jsbin.com/cubipigaqe/1/edit?js,console
                    Могу вернуть опцию, не проблема)


                    1. RubaXa
                      20.10.2016 18:27

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


                      1. Riim
                        20.10.2016 18:30

                        Я правильно понимаю, что в root под подпиской на items[0].value подразумевается подписка на сам список?


                        1. RubaXa
                          20.10.2016 18:37

                          Смотрите http://jsbin.com/zaserej/edit?js,console а теперь почитайте мои коменты к выводу:


                          ~~~ START ~~~                  //  Тут всё правильно
                          root: foo
                          item: foo + foo
                          item: foo + bar
                           ~~~ CHANGE: bar -> baz ~~~   // Супер, как и ожидалось!
                          item: foo + baz
                           ~~~ CHANGE: foo -> oof ~~~   // И...
                          item: foo + oof               //  <------  эх, ты лишний!
                          root: oof                     // Далее всё норм
                          item: oof + oof
                          item: oof + baz


                          1. Riim
                            20.10.2016 18:46

                            А точно ли он лишний? Реакт ведь не создаёт новые инстансы компонентов для элементов списка при повторном рендере, а как-то совмещает их с предыдущими. Если так, то лишний кто-то из двух последних, а другой из них должен начинаться с 'foo'. Вроде так, нет?


                            1. RubaXa
                              20.10.2016 18:52

                              Да выбросите вы этот Реакт из головы, он тут совсем не причем.


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


                            1. RubaXa
                              20.10.2016 18:59

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


                              1. Riim
                                20.10.2016 19:07

                                Да не, нет всё таки проблемы)) Ячейки создаются в самой формуле при каждом её расчёте, это же вообще никак не нормально. Я даже не знаю к какой жизненной ситуации это можно прилепить и даже если найдётся такая ситуация, то там такое поведение скорее всего окажется как раз ожидаемым. Вот, я сделал что бы ячейки от первого расчёта подцеплялись в последующих: http://jsbin.com/hokurejije/1/edit?js,console, теперь всё правильно)


                                1. RubaXa
                                  20.10.2016 19:13

                                  Теперь совсем неправильно ;]

                                  Сейчас
                                  ~~~ CHANGE: foo -> oof ~~~
                                  item: foo + oof // тот самый лишний
                                  root: oof // окей, root обновился
                                  // А где теперь свежие значения
                                  // - oof + oof
                                  // - oof + bar


                                  1. Riim
                                    20.10.2016 19:23

                                    тот самый лишний

                                    неа, он обновился первым, потому что root зависит от него, он (sub) должен вычислиться раньше чтобы дать ему (root) актуальное значение (в данном случае оно не используется), иначе сам root вычислится от старого значения sub, а только потом sub обновится.


                                    А где теперь свежие значения

                                    foo запомнился в замыкании при первичном создании ячейки, тут уже проблема проброски данных (и вполне решаемая), а не РП.


                                    1. RubaXa
                                      20.10.2016 19:30

                                      Я честно не понял о чем вы, но добавил `map` и `return`, теперь ячейки возвращают какие-то полезные данные http://jsbin.com/vupika/edit?js,console и всё хорошо, пока до `foo => oof`.


                                      1. Riim
                                        20.10.2016 19:52

                                        Ну смотрите, есть три ячейки зависимые в таком порядке: A -> B -> C, изменяем A. Кто должен сперва вычислится B или C? Если сперва вычислится C, а потом B, то C прочитает устаревшее значение от B и вычислится неправильно. Примерно тоже самое и здесь. Проблема на самом деле намного хитрее, в статье про атомы vintage толково её описал (начиная с 2. Каскадное непротиворечивое обновление значений)


                                        пока до foo => oof

                                        в выводе сейчас вроде всё как надо.


                                        1. Riim
                                          20.10.2016 20:17

                                          Вот графически схема зависимостей: https://yadi.sk/i/6pID2uk7xFZ7s


                                        1. RubaXa
                                          20.10.2016 20:52

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


                                          1. Riim
                                            20.10.2016 21:10

                                            А что за ситуация в которой понадобилось при каждом расчёте создавать ячейки? Это какая-то реальная ситуация или просто ради эксперимента?


                                            1. vintage
                                              20.10.2016 23:08
                                              +2

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


                                              1. обновили значение первого элемента.
                                              2. инвалидировалось значение корневой вьюшки. так как она корневая, то запланировала отложенное обновление.
                                              3. инвалидировалось значение первой вьюшки.
                                              4. сработало отложенное обновление корневой вьюшки.
                                              5. дальше всё как и при инициализации.


                                              6. обновили значение второго элемента.
                                              7. инвалидировалось значение второй вьюшки.
                                              8. значение корневой вьюшки помечено как требующее проверки. так как она корневая, то запланировала отложенную проверку.
                                              9. сработала отложенная проверка корневой вьюшки.
                                              10. корневая вьюшка проверила первый айтем на актуальность состояния.
                                              11. корневая вьюшка проверила первую вьюшку на актуальность состояния.
                                              12. корневая вьюшка обнаружила вторую вьюшку в неактуальном состоянии.
                                              13. вторая вьюшка обновила своё состояние.
                                              14. так как значение второй вьюшки не изменилось (изменилось лишь побочное действие), то корневая вьюшка решила, что её состояние актуально и обновляться не стоит.


                                          1. vintage
                                            20.10.2016 22:44
                                            +2

                                            http://jsbin.com/pomijoyiwu/edit?js,console


                                            "~~~ START ~~~"
                                            "root: foo"
                                            "item: foo + foo"
                                            "item: foo + bar"
                                            " ~~~ CHANGE: bar -> baz ~~~"
                                            "item: foo + baz"
                                            " ~~~ CHANGE: foo -> oof ~~~"
                                            "root: oof"
                                            "item: oof + oof"
                                            "item: oof + baz"


                                            1. RubaXa
                                              21.10.2016 08:40

                                              Во! Вот это уже успех!
                                              Смело вычеркиваем `$mol_atom` из типичных реализаций РП ;]


                                            1. Riim
                                              21.10.2016 11:34

                                              @vintage, я правильно понял, что в схеме A -> B -> C при изменении A сперва вычислится C, потом B и если значение B при этом изменится, то C посчитается ещё один раз?
                                              Добавил чтобы sub возвращал выводимое значение, появилось что-то лишнее: http://jsbin.com/mabozejudi/1/edit?js,console


                                              1. vintage
                                                21.10.2016 15:47
                                                +1

                                                Нет, там две фазы: сначала всплытие проверок до корня, а потом уже обновление инвалидированных атомов в правильном порядке. У каждого атома есть 3 состояния: actual, obsolete, checking. Если атом obsolete, то при обращении к нему происходит обновление. Если checking, то предварительно актуализируются его завсимости и если хоть одна при этом поменяется, то она сделает его obsolete, что приведёт к его обновлению.


                                                Ничего лишнего, rootLevel у вас зависит от значений возвращаемых subLevel.


                                                " ~~~ CHANGE: bar -> baz ~~~"
                                                "item: foo + baz" // вычислили второй сублевел
                                                "root: foo" // вычислили корень
                                                "item: foo + foo" // создали новый первый сублевел и вычислили его
                                                "item: foo + baz" // создали новый второй сублевел и вычислили его
                                                // уничтожились старые сублевелы

                                                Чтобы не пересоздавать атомы — их нужно кешировать.


                                                1. Riim
                                                  21.10.2016 17:07

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


                                                  1. Riim
                                                    21.10.2016 17:09

                                                    Непонятно почему изменился порядок расчёта после push: http://jsbin.com/xebozudala/1/edit?js,console? Видимо ещё хитрее)


                                                  1. vintage
                                                    21.10.2016 20:34

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


                                                    1. Riim
                                                      22.10.2016 01:55

                                                      … А если В не изменит значение, то С не будет перевычисляться ...

                                                      ну да, то что там защита от лишних расчётов это понятно. Я экспериментировал с таким вариантом, не ради другого поведения, а интересно было, что по скорости из него можно было выжать. Получалось не очень, много потерь на обходе для инвалидации ячеек перед их вычислением. Сейчас у меня похожим образом работает пассивный режим, но без этого обхода (его и не может быть, сам смысл пассивного режима в отсутствии ссылок от родителей к потомкам, чтобы GC мог забирать такие ячейки, а значит и обходить просто не по чему). Сделано немного иначе: есть общая для всех переменная, которая при каждом проходе расчёта инкрементится и все попавшие в этот обход ячейки получают это значение. У оставшихся ячеек это значение уже отстаёт, это и делает их устаревшими. Минус в том, что устаревшими могут случайно становиться лишние ячейки, ну и естественно случаются лишние расчёты, но тут уже ничего не поделать, при отсутствии ссылок обойти и точно пометить кто устарел, а кто нет, не выйдет. Для пассивного режима этого хватает, для основного конечно не подойдёт.


                                                      1. vintage
                                                        22.10.2016 10:26

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


                                                        А в чём практический смысл пассивного режима? Ведь он приводит к тому, что и вычисления все нужно проводить и ячейки все создавть.


                                                        Если потомок имеет ссылку на родителя, то в чём проблема пометить родителя устаревшим? Ссылка от родителя к потомку нужна для уничтожения подписок. Или в пассивном режиме вообще никаких ссылок нет?


                                                        1. Riim
                                                          22.10.2016 11:28

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

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


                                                          А в чём практический смысл пассивного режима? Ведь он приводит к тому, что и вычисления все нужно проводить

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


                                                1. xGromMx
                                                  21.10.2016 17:13

                                                  Когда статья?)


                                                  1. vintage
                                                    22.10.2016 10:27
                                                    +1

                                                    Когда кармы заработаю. :-)


  1. RubaXa
    23.10.2016 09:38
    +1

    Не знаю, кто есть живой в этом посте, но если вам не сложно проплюсуйте карму vintage и Riim. Я понимаю, что многим бывает не нравится назойливость и их манера вести диалог (без обид парни), но они наверно единственные, кто серьезно исследует тему настоящего РП (на хабре уж точно), а не тормозных стримов в духе Rx. Поэтому я хотел бы видеть ещё статьи от них, с выводами которых можно не соглашаться, но проблемы и идеи которые они озвучивают близки мне, а то чтения «Готовим Реакт», «Полное руководство по Реакт», «Ехал Реакт, через Redux, Реакт, Реакт» в которых нет абсолютно ничего нового, уже выть хочется.