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

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

К примеру такой код (jsfiddle):

var Example = React.createClass({
    getInitialState: function() {
        return {
            showCounter: false,
            counter: 0
        };
    },

    tick: function() {
        this.setState({ counter: this.state.counter + 1 });
    },

    componentDidMount: function() {
        this.interval = setInterval(this.tick, 1000);
    },

    componentWillUnmount: function() {
        clearInterval(this.interval);
    },

    render: function() {
        console.log('render');
        return <div>{this.state.showCounter ? this.state.counter : 'counter is hidden'}</div>;
    }
});

Здесь по срабатыванию console.log-а можно видеть, что рендер и патчинг происходят при каждом изменении counter-а, при том, что никакого смысла в этом просто нет. Механизм состояний React-а не умеет сам грамотно оптимизировать такие ситуации.

Кроме того каждый компонент, для отслеживания момента когда ему нужно перерендерится, следит только за своим состоянием, если просто использовать в рендере какое-то свойство из стора приложения, то он просто не будет реагировать на изменение этого свойства. Для того, что-бы это происходило нужно «протаскивать» все используемые свойства из стора в сам компонент, делая их собственными свойствами. С использованием модулья fluxible это может выглядеть как-то так:

@connectToStores([FooStore],  {
    FooStore: function(store, props) {
        return {
            foo: store.getFoo()
        }
    }
})
class ConnectedComponent extends React.Component {
    render() {
        return <div/>;
    }
}

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

Естественно в любом более-менее сложном приложении таких свойств получается довольно много и их постоянное «протаскивание» в каждый компонент начинает неслабо напрягать.

Синтаксис установки значений через setState тоже явно проигрывает в юзабельности стандартной конструкции:

this.counter = значение;

Критиковать можно довольно долго, но какой в этом смысл, если не предлагать альтернативы. Для себя в качестве механизма состояний в приложениях я давно использую другой свой велик — cellx. Это довольно мощная и, что самое главное, очень быстрая реализация реактивного программирования для javascript. Эта статья не про него, но если есть сомнения в его крутости, то можно почитать ридми на русском, там описаны его основные фичи и оптимизации. При чтении можно задавать вопрос «что из этого есть в механизме состояний React-а», думаю сомнения быстро отпадут.

Оказалось, что React и cellx довольно легко интегрируются друг с другом, по сути нужно просто создать вычисляемую ячейку (в терминологии cellx-а) в формуле которой вызвать render, в componentDidMount подписаться на неё, а в componentWillUnmount отписаться. Что бы каждый раз не писать всё это и окончательно облегчить себе жизнь, я написал простейший декоратор, делающий это за меня: react-bind-observables.

Всё было хорошо, я что-то писал на этой связке и не знал проблем, но однажды мой коллега рассказал мне про библиотечку morphdom. Суть библиотеки в том, что она делает тоже самое, что и React, но не с Virtual DOM, а с обычным dom-деревом. Как ни странно, оказалось, что делает она это ещё и довольно быстро, в среднем выходит почти в два раза быстрее React-а (ссылки на бенчмарки в репозитории morphdom). На самом деле, если разобраться подробнее, то такая скорость получается в основном за счёт заметно более быстрого рендера, склеивание строк и emptyElement.innerHTML = 'склеенная строка' срабатывают сильно быстрее чем массовое создание на каждый элемент и текстовый узел кусочков виртуального dom-дерева. А вот сам патчинг у morphdom-а всё же медленнее (в сумме получается опять же быстрее). Всё это можно в подробностях рассмотреть на этом независимом бенчмарке.

В тоже время morphdom умеет только патчить dom, в нём нет никакого механизма состояний и никакой системы компонентов. Ну и меня, конечно же, понесло.

Расскажу как всё это собиралось по порядку. Первым делом нужна какая-то компонентная система. В идеале всё должно быть примерно как в React-е, то есть отрендерили элемент <my-widget></my-widget> и какая-то логика сразу применилась к нему. Никаких $('.selector').myWidget() как в jQuery быть не должно, то есть всё должно быть в декларативном стиле. Тут я вспомнил про MutationObserver. Работает получившаяся система примерно так.
Объявляем логику для компонентов:

rista.component('hello-world', {
    init() {
        // Сработает при появлении элемента `<hello-world/>` в документе.
        // `this.block` - ссылка на появившийся элемент.
    },

    dispose() {
        // Сработает при удалении элемента `<hello-world/>` из документа.
    }
});

Дальше, по готовности dom, фреймворк вызывает функцию initComponents с аргументом document.body, которая составляет css-селектор из всех объявленных компонентов, делает выборку по нему (селектору) из переданного элемента и для каждого найденного элемента создаёт экземпляр соответствующего класса-компонента, сгенерированного вызовом rista.component. В конструкторе класса вызывается его метод init. Дальше фреймворк, используя MutationObserver начинает следить за всем, что появляется в документе и удаляется из него. При появлении новых элементов опять же применяет к ним initComponents, при удалении destroyComponents, который тем же способом находит в переданном элементе все «компонентные» элементы и вызывает dispose на соответствующих им экземплярах класса.

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

Идём дальше. Компонент должен уметь не только использовать контент своего корневого элемента, но и задавать собственное содержимое для него.

Тут как-раз и нужен render, который первый раз вызывается перед init-ом и должен вернуть строку (массив строк). Эта строка просто записывается в корневой элемент компонента (доступен как this.block) через innerHTML.

Теперь остаётся только понять когда нужно перерендеривать содержимое и научится как-то применять изменения без дальнейшей записи в innerHTML (я думаю не стоит объяснять, почему не стоит так делать). И тут, я думаю, уже всё понятно, с определением момента когда произошли какие-то изменения в состоянии всё разруливает cellx, так же как я описал выше для React-а, а вот обновления вносятся уже с помощью morphdom-а.

Остаётся ещё одна мелочь, morphdom применяя изменения может как-то пересортировывать элементы, временно удаляя их из dom и подставляя обратно в другом месте. Если при этом сразу применять initComponents и destroyComponents, то начнётся настоящая адуха с постоянным пересозданием уже инициализированных инстансов компонентов. Это надо как-то решать. Рецепт прост: при срабатывании MutationObserver-а не запускать initComponents и destroyComponents сразу, а просто регистрировать добавленные и удалённые элементы в соответствующих коллекциях. При этом когда элемент добавляется, не регистрировать его сразу в добавленных, а сперва искать его в коллекции удалённых и если он там есть, значит он только-что был удалён и теперь возвращается обратно, нужно просто удалить его из коллекции удалённых никуда не добавляя. При первом таком срабатывании ставится nextTick (setImmediate) с обработчиком, который сработает как только morphdom доделает свои манипуляции, в нём уже и делается обработка полученных коллекций. Как сказал один товарищ, лучше покажите мне код:

let removedNodes = new Set();
let addedNodes = new Set();

let releasePlanned = false;

function registerRemovedNode(node) {
    if (addedNodes.has(node)) {
        addedNodes.delete(node);
    } else {
        removedNodes.add(node);

        if (!releasePlanned) {
            releasePlanned = true;
            nextTick(release);
        }
    }
}

function registerAddedNode(node) {
    if (removedNodes.has(node)) {
        removedNodes.delete(node);
    } else {
        addedNodes.add(node);

        if (!releasePlanned) {
            releasePlanned = true;
            nextTick(release);
        }
    }
}

function release() {
    releasePlanned = false;

    if (removedNodes.size || addedNodes.size) {
        // здесь обрабатываем removedNodes и addedNodes
    }
}

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

Что в результате


За последние 2-3 недели я написал целую кучу разных мелких приложений уровня тудулиста, в одном проекте, который пилю для себя, заменил React на Rista, написал несколько компонентов, некоторые уже выложил на гитхаб: popup, switcher, router.
Общие впечатления от процесса примерно те же, что и от использования связки React+cellx, есть конечно некоторые минусы вроде таких:

  • нельзя закрывать компоненты так <popup/>, приходится писать полноценный закрывающий тег;
  • в компоненты нельзя передавать значения ссылочного типа, только строки.

В целом, значимых минусов пока не обнаружил. Основные плюсы получились следующие:

  • В разы легче React-а, после минификации и gzip-а всего 10kB.
  • Быстрей. Тут сложно определить какие-то конкретные цифры, morphdom быстрей аналогичного механизма в React-е почти в два раза, скорость cellx-а зависит от конкретной ситуации, но если в подобном тесте React окажется в 5 раз медленнее, то это уже будет отличным результатом для него.
  • Не требует какой-то предварительной обработки кода (jsx), можно просто подключить js-файлик к проекту и написать какой-то компонент, который сразу начнёт работать.

Вроде всё. Если есть идеи по дальнейшему развитию получившегося фреймворка, не стесняйтесь создавать issue на github-е (лучше на русском, мой бурятоанглийский оставляет желать лучшего).

Всем спасибо за внимание.

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


  1. Razaz
    02.12.2015 12:20
    +2

    Angular — Angular Light — React — React Light. Может уже остановиться на виджетах jQuery? :)


  1. Leopotam
    02.12.2015 12:35
    -4

    WebComponents не пробовали?


    1. Riim
      02.12.2015 13:46

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


  1. Finom
    02.12.2015 13:01
    +1

    Простите за рекламу, но проблема с состояниями давно решена в Матрешке.


    1. Riim
      02.12.2015 13:50
      -3

      Первые наработки по реактивности у меня появились ещё до появления knockoutjs, в котором я впоследствии подсмотрел кое-какие идеи. Матрёшка, наверное, помоложе будет.


      1. Riim
        03.12.2015 11:05
        +3

        Хм, судя по минусам, фанаты матрёшки не понимают зачем нужен cellx, ведь всё это давно решено в матрёшке. Ок, посмотрим подробнее.

        Первое что обычно стоит проверить, умеют ли вычисляемые свойства «схлопывать» несколько одновременных изменений своих зависимостей в одно собственное перевычисление.
        Так это выглядит на cellx-е: jsfiddle.net/nw117z8u.
        При изменении обоих зависимостей перевычисление свойства `c` произошло только один раз, что видно по единственному срабатыванию console.log-а.
        Что скажет нам матрёшка: jsfiddle.net/x7ybauak.
        Два console.log-а — фейл.
        Почему это важно? Дело в том, что это наиболее частая причина тормозов при активном использовании реактивного программирования, в данном случае два перевычисления, одно из которых заведомо лишнее и пока ничего страшного, но при удлиннении цепочки зависимостей число лишних срабатываний будет расти в геометрической прогрессии. Количество вопросов по поводу тормозов knocknoutjs-а на stackoverflow хорошо показывает к чему это приводит.

        В принципе уже здесь можно отложить дальнейшие исследования, так как огромное число кейсов уже сейчас заведомо непроходимы для матрёшки, но попробуем что-нибудь попроще. Скажем такой пример: jsfiddle.net/4k48zgkj. Уже нет вычисляемого свойства, только обычное-наблюдаемое, и опять же два срабатывания обработчика изменения, при том, что смысла нет ни в одном из них, так как значение сразу приходит к исходному. Что скажет cellx: jsfiddle.net/sd6pf99a. Как и положено — тишина в консоли.

        Может матрёшка умеет динамически актуализировать зависимости вычисляемых свойств и тем самым опять же избавляться от лишних вычислений? Учитывая, что в linkProps приходиться ручками указывать зависимости, скорей всего нет, но всё же проверим: jsfiddle.net/x3o9prsf. Хм, сработал один console.log и это отлично, видимо что-то она всё же умеет, но попробуем чуть изменить пример добавив console.log в саму формулу вычисления: jsfiddle.net/87y46y8g, и опять фейл — вместо положенных двух вычислений (инициализирующее вычисление и после `this.a = true`) сразу четыре.

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

        Идём дальше, знает ли вычисляемое свойство в матрёшке, что ему нет смысла перевычисляться, когда на него никто не подписан: jsfiddle.net/kLqghq3w. И опять фейл. На первый взгляд опять же ничего страшного, здесь никакого геометрического роста лишних перевычислений не случается, но что будет если master-свойство находится в одном объекте, а вычисляемое из него в другом? Скажем master-свойство в основной модели прила, а вычисляемое свойство оказалось слишком специфичным для общей модели и его решили определить в самом компоненте. Дальше компонент убивается (пользователь закрыл какую-то плашечку), конечно же снимаются все обработчики и вот тут самое интересное — вычисляемое свойство просто зависает в памяти. Какого-то способа как-то хотя бы вручную отписать его от основной модели в доке фреймворка я не увидел, судя по коду его просто нет. Так течёт память в knocknoutjs-е и так же она будет течь в матрёшке.
        Да и в целом, глядя на то, как автор матрёшки работает в ней с памятиью, я очень сомневаюсь, что ему приходилось писать действительно большие приложения.

        Вывод: реализация реактивности (механизм состояний) в матрёшке — полный фейл.


  1. agatische
    02.12.2015 13:23

    shouldComponentUpdate не пробовали?


    1. Riim
      02.12.2015 13:51
      -1

      В курсе что это, но не понимаю как он может мне помочь. Расскажете?


      1. agatische
        02.12.2015 13:56
        +1

        Здесь по срабатыванию console.log-а можно видеть, что рендер и патчинг происходят при каждом изменении counter-а, при том, что никакого смысла в этом просто нет. Механизм состояний React-а не умеет сам грамотно оптимизировать такие ситуации.


        shouldComponentUpdate поможет в этом случае.


      1. hell0w0rd
        02.12.2015 13:59

        Здесь по срабатыванию console.log-а можно видеть, что рендер и патчинг происходят при каждом изменении counter-а, при том, что никакого смысла в этом просто нет. Механизм состояний React-а не умеет сам грамотно оптимизировать такие ситуации.

        Умеет. Для этого нужно дать подсказку в виде функции shouldComponentUpdate.


        1. Riim
          02.12.2015 14:07
          -1

          Точно, только прийдётся его результат вычислять ручками по свойству showCounter, в данном случае проблем нет, но при более сложной логике уже будет не так весело. cellx делает всё сам: Динамическая-актуализация-зависимостей.


          1. hell0w0rd
            02.12.2015 14:46

            Не уверен, что ваше решение лучше. У вас это делается не явно.
            Скажем так, есть возможность написать свой shouldComponentUpdate?
            В React можно подключить миксин в одну строчку, или сделать такое:

            function createPureComponent(definition) {
              return React.createComponent(makePure(definition));
            }
            


            1. Riim
              02.12.2015 14:59
              -1

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

              Скажем так, есть возможность написать свой shouldComponentUpdate?

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


      1. RubaXa
        02.12.2015 16:59
        +1

        Имеется виду, что вместо реактивного огорода, можно было сделать простенькую проверку, примерно такую:

        shouldComponentUpdate() {
          const showCounter = this.state.showCounter;
          const changed = showCounter || (this.__showCounter !== showCounter);
          this.__showCounter = showCounter;
          return changed;
        }
        

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

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


        1. Riim
          02.12.2015 23:16
          -3

          Рискну предположить, что полноценно реактивное программирование вы не использовали. С ним всё не так просто, довольно сложно на словах понять его преимущества и не достаточно просто посмотреть несколько примеров, тут нужно именно попробовать применить его в каком-то хотя бы средненьком проекте. Я бы сказал, нужно немного поменять мышление, и происходит это не сразу. В тоже время мнение тех, кто научился его применять, обычно сильно отличается от вашего, пару раз слышал что-то вроде: «не понимаю как вообще раньше что-то писал без этого». Очень многие вещи с ним получаются сильно проще, объём кода обычно отличаться в разы. Мне по работе часто приходится работать с проектом где нет ничего подобного, и, если честно, я мучаюсь, и вся команда мучается, просто они не знают об этом и на слово не верят :)

          проще и быстрей перерендерить всё

          с быстрей сильно не согласен, cellx на моём компе просчитывает 100000 вычисляемых свойств за 120мс. Сложно представить себе интерфейс в котором одновременно меняется такое количество свойств. Любой лишний рендер скушает заметно больше ресурсов (в расчёте, что одновременно поменялось 1-1000 свойств), получается не просто не быстрей, а ооооочень медленней. С другой стороны js-движки сейчас настолько быстрые, что и это «ооооочень медленней» на глаз просто не видно, так что почему бы и нет, если сложно от этого избавиться без реактивщины. Но и опять же, если она есть, то почему не избавиться? Как я написал в статье, просто вызываем рендер в формуле ячейки, по сути метод рендера становится формулой этой ячейки. И всё, всё что относится к ячейкам, начинает относиться и к рендеру: автоматическая актуализация зависимостей, о которой есть в статье, схлопывание нескольких изменений в одно событие (то есть при изменении нескольких сторов за раз реакт не просто всё перерендерит, а сделает это несколько раз подряд, риста же отрендерит только один раз и только там где надо), о чём я не стал писать и т. д. В тоже время если нет опыта использования вычисляемых свойств, то можно ими вообще не пользоваться, по возможностям получится тот же реакт.

          можно было сделать простенькую проверку

          это она пока простенькая (хоть и выглядит уже громоздко), так как пока только одно условие. С усложнением логики объём кода без реактивного программирования будет расти вовсе не линейно. Пример из реального проекта (упрощённый):
          Есть отображаемый список юзеров, у каждого есть свойство `sex` (мальчик или девочка) и `online` (онлайн ли он сейчас). Есть панель фильтров, в ней селектбокс для фильтрации по полу и чекбокс для фильтрации по статусу онлайна (если не отмечен, то выводим всех, если отмечен, то только тех кто онлайн). Оба фильтра могут использоваться одновременно. Оба свойства любого юзера могут меняться как угодно в любом месте приложения. То есть тут получается уже не просто одно свойство, за которым нужно следить, а по два свойства в каждом элементе списка.
          Вот так это выглядит на ристе: jsfiddle.net/a85gor37
          Попробуйте переписать без реактивного программирования, на том же реакте, таким образом, что бы код гарантировал отсутствие избыточных рендеров. Сам не пробовал, но теоретически кода должно получиться минимум в два раза больше, а его «понимаемость» местами упадёт ниже плинтуса.


          1. RubaXa
            03.12.2015 14:18

            Зря вы так, мне нравиться реактивность, но как идея из мира фей и единорогов. Во вторых сейчас ваш пример проигрывает React'у более чем в два раза и это при помощи примитивного EventBus:

            http://jsperf.com/rista-vs-react

            Если я где-то ошибся, то пожалуйста поправьте меня.


            1. Riim
              03.12.2015 16:09

              Зря вы так

              ну я не хотел как-то обидеть)), хотя перечитав свой коммент понимаю, что его начало действительно можно так понять. Сорри если что.

              Если я где-то ошибся, то пожалуйста поправьте меня.

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

              Что происходит в тесте: в первых же строках вы определяете process.nextTick таким образом, чтобы cellx подумал, что это нативная реализация и начал его применять. Причём nextTick написан синхронным, что по сути на корню обрубает любые попытки cellx-а что-то оптимизировать. Далее вы добавляете в рендер компонента rista-test-app кусочек `${appModel.filterOptionOnline? 'checked': ''}`, смысла в нём не много, учитывая, что этот атрибут задаёт только начальное состояние компонента и нет смысла рендерить при его изменении, но тем не менее в реальном приложении такое более чем возможно и программист не должен в таких фреймворках думать о том, что это приводит к каким-то лишним рендерам. И в данном случае, действительно приводит — этим кусочком вы окончательно заставляете ристу рендерить абсолютно всё и абсолютно на каждый чих (прям как в реакте). В результате тест сводится к чистому тестированию React-vs-morphdom. И morphdom оказывается медленней, я пока не понял почему, может в тесте ещё какая-то хитрось, которую я пока не рассмотрел или может все бенчмарки React-vs-morphdom врут. Ещё, вполне вероятно, я многовато напихал в колбеки morphdom-а, надо будет поисследовать этот момент подробнее (там действительно есть, что пооптимизировать).


              1. RubaXa
                03.12.2015 16:43

                Я знал, что вы так ответите, прям слово в слово :]

                Вы когда-нибудь пробовали в цикле вызывать setTimeout/setImmediate? Нет, обязательно попробуйте, результат вас расстроит. Сразу же отвечу, это очень дорогая операция и на 1000+ итераций тормоза существенные. А 1000 + это не какая-то мифическая задача, например для меня это обычное действие пользователя «Отметить все письме как прочитанные».

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

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

                В результате тест сводится к чистому тестированию React-vs-morphdom.

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

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


                1. Riim
                  03.12.2015 19:05

                  Вы когда-нибудь пробовали в цикле вызывать setTimeout/setImmediate?

                  Скорей всего имеется в виду не цикл, а рекурсивное создание таймаута/nextTick-а в его же колбеке. Да я в курсе, что даже в случае с nextTick-ом есть дополнительные расходы которые убивают производительность такого «цикла». Но к cellx-у это не имеет никакого отношения, несколько подряд сделанных изменений будут обработаны не друг за другом в несколько срабатываний nextTick-а, а всей кучей в nextTick-е, установленном при первом изменении.

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

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

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

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


  1. vintage
    02.12.2015 14:02
    +1

    Процитирую-ка я пожалуй себя:

    Я тут погонял тесты — они несколько оторваны от жизни. Всё же цепочки глубиной как правило не более пары десятков, а в тестах их получается аж до 25000. Тут нужно скорее увеличивать число зависимых атомов, а не глубину. Я бы предложил увеличивать число атомов на каждой глубине, например, вдвое, тогда c глубиной 13 общее число атомов будет уже 16000.

    Моя реализация совсем не оптимизирована под большие глубины, о чём говорят 2 и 4 строчки, съедающие половину всего времени:
    http://gyazo.com/ad5689819da7946bb6f28a1879ae2622

    В твоей реализации из-за замыканий идёт большая нагрузка на GC:
    http://gyazo.com/72faacabb0872300caf0ff6f53ffecb5

    Ещё я поправил код теста с jin-atom чтобы использовались прототипы:
    https://gist.github.com/anonymous/69a1a1531679e8f39aab

    Оптимизировал работу с глубокими цепочками: http://nin-jin.github.io/lib/props/props1.js
    Теперь моя реализация с замыканиями по скорости примерно сравнима с твоей, но вот реализация с прототипами раза в 2 быстрее.


    1. Riim
      02.12.2015 14:23

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


      1. vintage
        02.12.2015 15:08

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


        1. Riim
          02.12.2015 15:11

          Согласен.


  1. Riim
    02.12.2015 14:06

    Упс, не туда)


  1. jakobz
    02.12.2015 14:14
    +3

    Уже вторая статья с подобной логикой:
    — берем реакт, главная идея которого — что весь UI зависит от корневого стейта
    — почему-то появляется жим-жим насчет перерисовывать все на каждый чих
    — начинаем придумывать велосипед чтобы не перерисовывать весь UI, и возвращаемся назад к модели программирования «лапша из колбеков».

    В реакте нормально перерисовывать весь UI на каждый чих — это его парадигма, он ей и прекрасен. Как только у нас observable model появляется — парадигма ломается, и все превращается в такую же дрянь как ангуляр.


    1. Riim
      02.12.2015 14:25
      -2

      и возвращаемся назад к модели программирования «лапша из колбеков»

      можно подробнее? У меня вроде лапши не случается.


    1. denis_g
      02.12.2015 23:43

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


      1. vintage
        03.12.2015 00:01
        +1

        Там всё очень плохо http://habrahabr.ru/post/201832/


        1. denis_g
          03.12.2015 00:06

          Монументально. Спасибо.


          1. lega
            03.12.2015 21:49

            Там сделано «в лоб» — просто перебор «данных» и проверка с предыдущим значением (без всякой магии отслеживания), некоторым не нравится, но зато (на многих тестах) работает быстрее «конкурентов», например тест1, тест2.


  1. aTwice
    02.12.2015 23:04
    +1

    Rista — отличное название для ReactOS с интерфейсом Windows Vista


  1. mynameisdaniil
    02.12.2015 23:36

    Чего только не делают люди, чтобы не использовать Elm


    1. irium
      03.12.2015 07:39
      +1

      А еще лучше Redux :)


      1. xGromMx
        03.12.2015 13:55
        +1

        а еще лучше Rx :D


        1. Riim
          03.12.2015 16:15

          Он как и knockoutjs не умеет и половины оптимизаций cellx-а.


          1. xGromMx
            03.12.2015 20:43

            Где примеры, чего он не умеет?


            1. Riim
              03.12.2015 22:09

              Практически все фичи описанные в ридми как раз можно считать этими примерами. Можно взять примеры из комментария выше и переписывать их на Rx, линейно это делать не везде получится, так как cellx — это про потоки данных, а Rx про потоки действий (источниками данных являются события), но линейно и не нужно. Насколько я знаю Rx, из перечисленного там, он умеет только динамически определять зависимости. В данном тесте он выдаёт настолько же плохой результат, как и knockoutjs или angular.


              1. xGromMx
                03.12.2015 22:18

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


                1. Riim
                  03.12.2015 22:54

                  Не знаю хорошо это или плохо, если считаете, что плохо и есть идеи по доработке, буду рад услышать.


      1. Riim
        03.12.2015 16:18

        cellx никак не конфликтует с идеями flow-архитектуры, в нём, в общем-то, почти всё уже готово. Если я ничего не забыл, остаётся только запилить диспетчер на готовом EventEmitter-е.


        1. xGromMx
          04.12.2015 17:49

          конкурент github.com/DaQuirm/nexus :D


          1. Riim
            05.12.2015 11:57

            Как и в случае с Rx-ом предлагаю прогнать вашу реализацию через примеры из комментария выше и рассказать здесь о результатах. Вдруг у вас действительно толковая альтернатива.


  1. lega
    03.12.2015 23:12

    в подобном тесте ...
    Похоже, что вы не понимаете как работает Angular.js, как минимум нужно делать $watch, и вызывать $digest для отслеживания изменений. Вы же замеряете скорость Object.defineProperty, а не ангуляра — уберите ангуляр из теста и ничего не изменится (т.е. ангуляр на этот тест никак не влияет).


    1. Riim
      05.12.2015 11:49

      В ангуларе действительно мало что понимаю, делал по этой статье: angular-tips-computer-properties. Если не сложно, можете набросать пример как будет правильно?


      1. lega
        05.12.2015 12:31
        +1

        Ангуляр не для FRP, там по сути нужно только DOM «прибиндить», поэтому там установлено ограничение (равное 10) на кол-во циклов (глубина), и для Ангуляра этого вполне хватает.
        А тесты можно заточить под каждый фреймворк/библиотеку, вот например тест где cellx работает в 200-400 раз медленнее Ангуляра: cellx и angular.


        1. Riim
          05.12.2015 13:34

          А тесты можно заточить под каждый фреймворк/библиотеку

          Согласен, но плохой тест для cellx-а всё же получился некорректным, в нём сравнивается не cellx и angular, а cellx и скорость записи в свойство на голом js-е. Кроме того v8 без проблем разворачивает такой цикл в банальную конструкцию `scope.index = 999999;`. Более корректно будет вызывать scan в каждой итерации цикла: jsfiddle.net/c4j2odao, и вот ангулар уже в 5 раз медленнее.


          1. lega
            05.12.2015 13:59

            Сellx откладывает все в nextTick, тогда уж так и ангуляр опять быстрее.

            cellx и скорость записи в свойство
            Суть в том что ангуляр работает по простому — с «голыми» данными, без всяких хитрых (скрытых) обработчиков, которые есть у других, и когда все вычисления закончены — вызывается scan/digest, поэтому мой первый пример такой — он отражает этот подход.

            Есть тест поближе к реальности (старая копия на plunker), сделайте вариант для Rista что-б увидеть насколько он медленнее/быстрее React/Ангуляра и прочих.


            1. Riim
              05.12.2015 22:42

              поэтому мой первый пример такой — он отражает этот подход

              Он отражает лишь умение V8 оптимизировать такие циклы.

              Есть тест поближе к реальности (старая копия на plunker)

              Как я понял вы разработчик angular-light. Забавный у вас тест получился, другие фреймворки используют для работы с dom-ом какие-то свои механизмы, причём в лоб, без малейших попыток что-то оптимизировать, а для alight вы dom создаёте ручками на самом низком уровне, да и обновляете также, через textContent и заранее закешированную ссылку на элемент, прям милота-то какая!)) В результате alight конечно же всех жёстко рвёт, ну разве что нативный js более-менее держится. Ну что же, раз так можно, риста никак не мешает работать с dom-ом на низком уровне, вот plnkr.co/edit/4uaTO4Jpsq0MWCCvmS5o?p=preview, теперь раста быстрее даже нативного js-а в 5-9 раз).
              Всё это забавно конечно, но всё же, что вы хотите доказать этими тестами? О чём должны говорить результаты? Если фреймворк А оказался в два раза медленнее фреймворка Б, это значит интерфейс сделанный на нём будет в два раза медленнее? Да нифига это не значит, даже на слабеньких мобилках на глаз разницы никакой не будет. Тормозят фреймворки не из-за этого, а из-за наличия в основных механизмах каких-то ловушек производительности, в knockoutjs-е это длинные цепочки зависимостей, в angular-е — большое число биндингов, отмазка разработчиков: «2500 — хватит всем, ага» и т. д. Тот же реакт в тесте почти в два раза медленнее angular-а, но что-то я не видел тормозящих интерфейсов на нём, а вот на angular-е повидал не мало. Вот именно такие ловушки и есть смысл поискать и просто рендер длинного списка здесь плохой способ, такие списки очень хорошо выделяются в реальных приложениях, от фреймворка требуется лишь не мешать работать с ними на низком уровне. Причём даже если сейчас сделать тест, скажем с бешенным числом биндингов и какой-то фреймворк окажется в два раза медленней другого, то это опять же ни о чём не будет говорить, он должен просесть с сотни-тысячи раз (ну хотя бы раз в 30) чтобы это можно было засчитать как ловушку производительности. В общем, без обид, но по-моему туфта ваш тест, мозги людям пудрите, годится разве что для писькомерства и заманухи неокрепших умов, которые ещё сами не знают, что им нужно и хорошо ведуться на «он типа самый быстрый».


              1. lega
                06.12.2015 01:15

                а для alight вы dom создаёте ручками на самом низком уровне, да и обновляете также, через textContent и заранее закешированную ссылку на элемент, прям милота-то какая!))
                Вы не туда смотрите, вот исходники. Там нет работы с DOM, все по честному.
                А вот Angular.js 2 не удаляет элементы, вставляет их на следующей итерации.

                Ну что же, раз так можно, риста никак не мешает работать с dom-ом на низком уровне, вот
                Сделайте нормальный тест, сейчас вы просто вставили нарисованный кусок HTML (да и он обновляется не быстро):
                block.appendChild(range.createContextualFragment(
                    '<li>rt: ' + evt.value.join('</li><li>rt: ') + '</li>'
                ));
                

                Всё это забавно конечно, но всё же, что вы хотите доказать этими тестами?
                А вы? У вас в статье ссылка на тест и ещё один тест в гитхабе.

                а вот на angular-е повидал не мало
                Я не видел ни одного приложения, правильно написанного на Ангуляре, которое тормозит. А вот тормозное приложение можно «наклепать» на чем угодно.


                1. Riim
                  07.12.2015 09:59

                  Вы не туда смотрите, вот исходники

                  Спрятали работу с dom, но по результату видно, что работает всё так же.

                  Сделайте нормальный тест

                  Нормальный это как? Оформить отдельный компонент `fast-list` и спрятать его где-то? Почему вы не перепишите вариант на alight как написали на angular-е (с применением конструкции типа `<li ng-repeat=«item in items»>ng: {{item.t}}</li>` и т. д.)? Для себя вариант в лоб я написал и результатом остался доволен, хоть и понимаю, что этот результат ни о чём не говорит. Авторский вариант на ристе у вас есть, сделать его максимально медленным предлагаю уже самостоятельно.

                  да и он обновляется не быстро

                  Какие у вас результаты? У меня:
                  Angular.js — 600 177
                  Angular Light: fl — 182 133
                  Angular Light 0.7.15 — 425 164
                  Angular Light 0.8.4 — 369 140
                  JS 0 — 165 131
                  JS 1 — 140 128
                  Rista — 18 64

                  А вы? У вас в статье ссылка на тест и ещё один тест в гитхабе.

                  Тест на гитхабе говорит о том, что пофикшена главная ловушка производительности, присутствующая почти во всех реализациях реактивного программирования. В статье так же сказано, что стоит почитать ридми в котором говорится, что это не единственная исправленная проблема, за 3+ года в продакшене я их довольно много отловил, в ридми описано лишь основное.
                  В статье делается сравнение производительности реакта и morphdom-а, делается потому, что механизм патчинга morphdom-а изначально выглядит такой ловушкой, так же как и аналогичный механизм в реакте, но ребята сделавшие реакт всячески убеждают, что проблем с этим нет и, как показывает практика, они не врут, а значит и с morphdom таких проблем не будет. Пусть даже авторские бенчмарки окажутся притянутыми за уши и он на самом деле в 2-5 раза медленнее, это норм, в сочетании с выборочным рендером и отсутствием лишних рендеров вообще шикарно выходит.
                  Своими тестами я пытался показать, что ловушки производительности с большой вероятностью полностью отсутствуют, но вы так и не ответили на мой вопрос, о чём говорят результаты вашего теста?

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


                  1. lega
                    07.12.2015 12:14

                    Спрятали работу с dom,
                    Это фича подобных фреймворков — «спрятать» DOM, что-бы разработчик его не касался, этим должен заниматься фреймворк.

                    но по результату видно, что работает всё так же.
                    Работает так быстро, потому что там dirty-checking и сама библиотека очень хорошо оптимизирована. Angular 2 и Basis.js дают примерно такие-же результаты.

                    Почему вы не перепишите вариант на alight как написали на angular-е (с применением конструкции типа <li ng-repeat=«item in items»>ng: {{item.t}}</li>
                    Оно так и есть: <li al-repeat="item in items">jj: {{item.t}}</li> — см. исходники.

                    милота-то какая!)) мозги людям пудрите, годится разве что для писькомерства, нубам такие тесты показывать, упорно лепить идиота

                    говорить с вами становится всё менее приятно
                    А с вами — одно удовольствие.

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

                    PS: по словам известного гика — одинаковые вещи сравнивать честно.


                    1. Riim
                      07.12.2015 13:10

                      Это фича подобных фреймворков — «спрятать» DOM, что-бы разработчик его не касался, этим должен заниматься фреймворк

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

                      Я уже могу засчитывать, что вы так и не можете объяснить назначение этого теста?


                      1. lega
                        07.12.2015 18:01

                        Никто не заставляет использовать morphdom
                        Т.е. вставка строк в innerHTML — это ваш официальный подход в Rista? С таким апи он конкурент jQuery, а не React.

                        Работа с DOM — это одна из основных частей в данных фреймворках, а этот тест как раз показывает скорость работы с DOM.

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

                        Это конечно не плохо, но вы в статье утверждаете что Rista быстрее чем React, дак покажите это, не нравится тест выше, поучаствуйте в этом (наследник теста от разработчиков Реакта, они показывали его на презентации), который был создан как раз показать преимущество Реакта.

                        Если и этот не нравится, сделайте свой тест «от а до я» (от данных до DOM), а то что вы ускорили какую-то деталь в «машине», она быстрее не поехала, по крайней мере этого пока не видно.

                        К тому же вы соревнуетесь не с Ангуляром, а с Реактом (в первую очередь), ведь если Rista медленней Реакта, то зачем он нужен?


                        1. Riim
                          08.12.2015 13:13

                          ведь если Rista медленней Реакта, то зачем он нужен?

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

                          который был создан как раз показать преимущество Реакта

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

                          Т.е. вставка строк в innerHTML — это ваш официальный подход в Rista? С таким апи он конкурент jQuery, а не React.

                          Можно переписать с innerHTML на document.create*, как сделано у вас, результат тоже будет в точности как у вас.

                          Вы оптимизировали то с чем у программистов (возможно) нет проблем

                          Я много чего оптимизировал в cellx-е, именно этот тест показан в ридми как демонстрация решения наиболее частой причины тормозов в подобных либах. Не обязательно создавать цепочки бешеной длинны, достаточно запихать в формулу ячейки map массива и тут уже даже десяток её лишних перерасчётов (а это цепочка длинной всего 3-5) может создать вполне ощутимые тормоза. Я погуглил за вас: groups.google.com.

                          Это конечно не плохо, но вы в статье утверждаете что Rista быстрее чем React, дак покажите это, не нравится тест выше

                          Цитата из статьи: «Быстрей. Тут сложно определить какие-то конкретные цифры»
                          В статье я даю ссылки на авторские бенчмарки morphdom-а, им действительно не стоило сильно верить, но понимая бессмысленность я даже перепроверять не стал, пусть он оказался бы даже в 5 раз медленнее, это вполне нормально. Вчера всё же проверил, на моих тестах выходит на 25-30% медленнее, примерно тоже самое на обоих предложенных вами тестах, всё норм, я доволен. Плюс cellx не даст рендериться лишнему и лишние разы, теперь вообще можно спать спокойно.

                          Если и этот не нравится, сделайте свой тест «от а до я» (от данных до DOM)

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

                          а то что вы ускорили какую-то деталь в «машине», она быстрее не поехала

                          Так себе вы в машинах разбираетесь)).


                  1. xGromMx
                    07.12.2015 13:59
                    +1

                    Кстати да, хотелось бы увидеть профит от cellx против most


                    1. Riim
                      07.12.2015 14:17

                      Вы теперь будете перечислять все подряд библиотеки с реактивным программированием?)) Я выше написал как самостоятельно проверить. К тому же most опять же из области event driven reactive programming, реализация которого строится уже поверх FRP и эту самую FRP-составляющую обычно плохо описывают в доке. В общем, у меня не настолько много времени, что бы разбираться со всеми предлагаемыми библиотеками, с чистыми представителями FRP ещё можно было бы поковыряться и сравнить, а здесь извиняйте)).


                      1. xGromMx
                        07.12.2015 19:56

                        Ваша библиотека тоже далека от FRP, походу вы не понимаете, что это такое.


                        1. Riim
                          08.12.2015 11:58

                          Вы правы, замените в комменте выше FRP на RP и всё будет правильно.