Warp9 — еще одна реактивная js библиотека. На этот раз компонуемая и без утечек

    Статья устарела, актуальную информацию ищите на сайте Warp9'а



    Существует множество реактивных и около-реактивных библиотек для создания графического интерфейса на js: Angular, Knockout, React, RxJS… Спрашивается, зачем писать еще одну. Оказывается, во всех них, помимо фатального недостатка, есть еще несколько.

    AngularJS Knockout React Elm RxJS, Bacon ReactiveCoffee Warp9
    template based yes/no[1] yes no no no no
    controls composability no[2][3] no[2] yes yes yes yes
    modularization of controls yes/no[4] no yes maybe yes yes
    2 way binding yes/no[5] yes no yes yes yes yes
    accidental memory leaks no yes no no no yes no
    todomvc yes yes yes no maybe yes/no[6] yes
    headless no yes no no yes yes yes
    pure js yes yes yes[7] no yes no yes
    mixing markup and logic yes/no[8] no yes yes yes yes
    simplicity no yes yes yes yes yes yes
    rich reactive list no no yes no no yes
    Так как RxJS и Bacon.js являются библиотеками для управления событиями, и не предоставляют примитивов облегчающих создание интерфейсов, то некоторые вопросы к ним просто не применимы. В принципе, брать их за основу проектирования GUI немного нелепо, но я все же включил их в таблицу, так как есть большой шанс, что их кто-то упомянет в комментариях.

    Расшифровка легенды и ремарки


    template based
    Являются ли шаблоны основным приемом при построении интерфейсов.

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

    controls composability
    Предоставляет ли библиотека механизм для сознания контролов через композицию существующих (принцип разделяй и властвуй).

    [2] В AngularJS и Knockout есть механизмы для включения шаблонов (ng-include, template-binding), кроме того шаблоны можно класть в разные файлы, но из этого не следует поддержка композиции контролов и модульности. Проблема в том, что сложное JS приложение это не только шаблон, но и логика, а логика в AngularJS отделена от шаблонов (в шаблонов, мы должны всегда помнить о логике (на лицо сайд эффект), но если так, то принцип разделяй и властвуй не работает, следовательно, композиция не дает нам преимуществ, следовательно ее нет.

    Резюме, нельзя говорить, что шаблоны компонуются, если с ними связана логика и она отделена от них.Я далеко не единственный, кто это заметил, создатели React пишут:
    “React doesn't use templates… ...React is a library for building composable user interfaces.”
    Если вы еще сомневаетесь, то задумайтесь, почему так мало шаблонных движков для создания десктопных, iOS или Android приложений.

    [3] Если создавать приложения используя только (!) кастомные директивы, то AngularJS поддерживает и controls composability, и modularization of controls.

    modularization of controls
    Можно ли оформить контролы так, чтобы они были самодостаточны и их можно было бы распространять независимо от приложения. Так как в web’е системы дистрибьюции есть только для javascript (requirejs, commonjs), то этот вопрос следует читать так, можно ли контролы завернуть в requirejs или commonjs.

    [4] Только если контролы AngularJS оформлять как кастомные директивы.

    2 way binding
    Возможно ли связать две переменные или переменную и контрол так, чтобы при изменении переменной изменялась и вторая (или контрол), и наоборот.

    [5] В AngularJS возможен биндинг между контролами и переменной, но невозможен между переменной и переменной.

    accidental memory leaks
    Легко ли нарваться на утечки памяти в приложении, написанном при использовании данной библиотеки. Так получилось, что ответ 'да' так же означает, что код с утечками выглядит значительно проще, чем эквивалентный код без утечек, а значит, если использовать эти библиотеки правильно, то они теряют часть своей элегантности.

    Я написал простое приложение на Knockout и ReactiveCoffee, чтобы продемонстрировать, насколько легко допустить утечки при их использовании, собственно,приложение на Knockout иприложение на ReactiveCoffee.И, конечно же, контрольноеприложение без утечек на Warp9.

    todomvc
    TodoMVC является стандартным hello world’ом для разнообразных gui библиотек на js, правилом хорошего тона считается иметь свою реализацию приложения в арсенале примеров.

    [6] TodoMVC на ReactiveCoffee реализован только частично: нет переключения вкладок (All, Active, Completed) и нет работы с local storage.

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

    pure js
    Предполагалось ли автором библиотеки, что основным языком, который будет использовать библиотеку будет js.

    [7] Создатели React предлагают использовать диалект javascript’а с поддержкой встроенной html-подобной разметки (подобно поддержки xml в scala) и компилить его в js. Использовать чистый js возможно.

    mixing markup and logic
    Предполагается ли, что для написания приложения нужно смешивать разметку и код.

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

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

    Есть другой путь для борьбы со сложностью — выделяем независимое поведение (разметка + логика) в контрол (абстракция) и, используя средства композиции, собираем из уже существующих контролов все более сложные, вплоть до нашего приложения. Проводя аналогию с оценкой сложности алгоритмов, разделение на логику и разметку уменьшает сложность вдвое (n -> n/2), в то время как абстракция/композиция сводит её с n до ln(n).

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

    [8] При разработке директив в AngularJS сталкиваешься с необходимостьюсмешивать логику и представление.

    simplicity
    Мало ли кол-во сущностей, которое вводит библиотека.

    Это очень субъективное мнение, но мне кажется, что количество сущностей в AngularJS слишком велико. Разработчики React разделяют это мнение:
    Number of concepts to learn
    • React: 2 (everything is a component, some components have state). As your app grows, there's nothing more to learn; just build more modular components.
    • AngularJS: 6 (modules, controllers, directives, scopes, templates, linking functions). As your app grows, you'll need to learn more concepts.

    rich reactive list
    Практически все реактивные библиотеки предоставляют примитивы для реактивных переменных, такие как создание новой реактивной переменной связанной функцией с другой реактивной переменной. В knockout это можно сделать так:

    var a = ko.observable(0);
    var b = ko.compute(function() { return a() + 2 });

    А в warp9 так:

    var a = new Cell(0);
    var b = a.lift(function(a) { return a+2; });

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

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

    var list = new List([new Cell(0), new Cell(1), new Cell(2)]);
    var sum = list.reduce(0, function(a,b) { return a + b; });

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

    Warp9


    В основе warp9 лежит очень простая идея: а давайте возьмем реактивную модель, применим к ней преобразование и получим связанный с моделью реактивный DOM. Если преобразование — простая js функция, то мы получим привычное средство композиции (композиция функций) и дистрибьюции (requirejs) из коробки.

    Пример отображения реактивной модели в реактивный DOM — приложение(исходник):

    // создаем реактивную модель (в данном случае список)
    var model = new List();
    // отображаем её в реактивный DOM
    var dom = TRANSFORM(model);
    // отображаем реактивный DOM
    warp9.ui.renderer.render(placeholder, dom);
    
    // изменяем модель
    // и изменения отображаются на экране
    model.add("React.js");
    model.add("Warp9");
    
    function TRANSFORM(model) {
        var hasItems = model.count().when(function(count) { return count>0 }, true);
        return hasItems.when(true, ["div",
            ["div", "Reactive libraries:"],
            ["div", {"css/margin-left": "10px"},
                model.lift(function(item){
                    return ["div", item]
                })]
        ]);
    }

    Реактивные примитивы


    Начнем знакомство с warp9 с реактивных примитивов, на основе которых и создана GUI часть библиотеки. В презентации достаточно подробное их описание.


    Все примеры из слайдов доступны на github'е.

    Подписки и утечки


    Реактивная модель (observer pattern, publish-subscribe) местами очень удобна, но не стоит думать, что решая одни проблемы она не вносит других. Одна из ахилесовых пят реактивности — утечки памяти. Если мы пойдем на вики почитать про утечки памяти, то среди прочего мы увидим:
    To prevent this (memory leaks), the developer is responsible for cleaning up references after use… and, if necessary, by deregistering any event listeners that maintain strong references to the object
    Это проблема относится ко многим имплементациям observer'а. Её упоминает Мартин Фаулер, ей посвящены статьи.

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

    Давайте посмотрим, как этот принцип реализуется на практике. Взглянем на следующий код:

    var cell = new Cell(0);
    var lifted = cell.lift(function(x) { return x+1; });

    Этот код выглядит совершенно обычно, поэтому естественно ожидать, что cell не содержит ссылки на lifted и когда все ссылки на lifted затрутся — объект соберется сборщиком мусора. В warp9 это действительно так, а теперь посмотрим на knockout:

    var cell = ko.observable(0);
    var lifted = ko.computed(function(){ return cell() + 1; });

    Тут cell будет содержать ссылку на lifted, что контринтуитивно. Рассмотрим код посложнее

    var cell = new Cell();
    var lifted = cell.lift(function(x) { return x+1; });
    var dispose = lifted.onEvent(Cell.handler({
        set: function(value) { console.info(“set: ” + value); },
        unset: function() { console.info(“unset”); }
    }));
    cell.set(3);
    cell.set(5);
    cell.unset();
    dispose();
    cell.set(1);

    Естественно было бы ожидать от него

     unset
    > set: 4
    > set: 6
    > unset

    Но такое «естественное» поведение требовало бы, чтобы cell содержал ссылку на lifted, а это нам не подходит, так как оно является потенциальной утечкой. Поэтому был сделан выбор — пусть лучше код, который выглядит хорошо не работает, чем работает, но содержит утечку; так как обнаружить неработающий код можно намного быстрее, чем обнаружить утечку.

    Чтобы код выше стал работающем — нужно вставить «подсказку» читателю, что он может течь:

    var cell = new Cell();
    var lifted = cell.lift(function(x) { return x+1; });
    var dispose = lifted.onEvent(Cell.handler({
        set: function(value) { console.info(“set: ” + value); },
        unset: function() { console.info(“unset”); }
    }));
    lifted.leak();
    cell.set(3);
    cell.set(5);
    cell.unset();
    dispose();
    lifted.seal();
    cell.set(1);

    Мы добавили lifted.leak() и lifted.seal(), код стал выглядеть подозрительно и, магическим образом, он заработал. На самом деле, вызов leak не магия, а активация реактивной переменной. При активизации происходит подписка на свои зависимости и их активация. Таким образом, вызывая leak у одного объекта (переменной или списка), мы транзитивно активизируем все его зависимости. После того, как мы закончили работать с активизированным нами объектом мы должны оставить его в том состоянии, в котором он нам достался — вызвать метод seal.

    Повторный вызов leak не приводит к повторной активизации, а всего лишь инкриминирует внутренний счетчик. Если leak был вызван n раз, то seal тоже должен быть вызван n раз; каждый вызов seal будет декриминировать внутренний счетчик и когда он достигнет 0 — произойдет фактическая дезактивация. Это было сделано, чтобы вы не думали в какой состоянии вам передают объект — вызывайте leak и seal парно и все будет хорошо.

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

    Исходя из примера выше можно было догадаться, что при подписываясь на переменную мы можем ожидать событий set и unset, у списка они другие: data, add, remove

    var list = new List();
    list.leak();
    list.onEvent(List.handler({
        data: function(items) {
            // событие вызывается время от времени, в items - все элементы
            // в списке на момент получения события; событие стоит воспринимать,
            // как "забудь все, что знал, сейчас контент списка такой"
            // items - массив объектов { key: key, value: value } где value -
            // значение вставленное в список, а key - ключ (id), по которому
            // объект можно удалить в исходном списке
        },
        add: function(item) {
            // вызывается, когда в слисок добавляется элемент, item имеет вид
            // {key: key, value:value}, value - новый элемент, key - его ключ
        },
        remove: function(key) {
            // вызывается при удалении элемента, key - его ключ
        }
    }));

    Разметка


    Рассмотрим классическое hello world приложение — пользователь вводит свое имя, например, Denis и видит реакцию «Hello, Denis!». Для того, чтобы продемонстрировать больше возможностей я добавил еще и кнопку, которая очищает поле ввода.

    Живое приложение доступно по ссылке (его исходники).

    //<html>
    //<body>
    //    <div id="placeholder"></div>
    //</body>
    //<script src="../../../lib/warp9/warp9.js"></script>
    //<script language="javascript" type="text/javascript">
        var Cell = warp9.reactive.Cell;
    
        warp9.ui.renderer.render(placeholder, NAME(new Cell("Denis")));
    
        function NAME(name) {
            var hello = name.when(
                function(name) { return name.length > 0; },
                function(name) { return "Hello, " + name + "!"; }
            );
            return ["div",
                ["div",
                    ["input-text", {
                        "css/background": hello.isSet().when(false, "red")
                    }, name],
                    ["button", {"!click": function() {
                        name.unset();
                    }}, "clear"]],
                hello.lift(function(hello) {
                    return ["div", hello];
                })
            ];
        }
    //</script>
    //</html>

    Из примера видно, что функция NAME отображает модель (реактивную переменную) в некоторое описание объектной модели, которое затем рендерится внутри div'а с id «placeholder». Само описание очень напоминает s-expression lisp'а, только вместо круглых скобок мы используем квадратные, можно пошутить и назвать их z-expression («z» относиться к «s» также, как "[" относиться к "(").

    Грамматика z-expression примерно следующая:

    explicitChildren = '["' TAG_NAME '"' (',' ATTR)? (',' child)* ']'
    implicitChildren = '["' TAG_NAME '"' (',' ATTR)? (reactive-list-of node) "]
    child            = node | '"' STRING '"' | INT |
                       (reactive-variable-of node) |
                       (reactive-variable-of STRING) |
                       (reactive-variable-of INT)
    node             = explicitChildren | implicitChildren

    где TAG_NAME — имя «html» тега (в кавычках, так как можно определять собственные теги), STRING — любая строка, INT — любой integer, ATTR — объект описывающий аттрибуты тега, css и события.

    Свойства объекта ATTR отображаются в аттрибуты конструируемого DOM элемента, значением могут быть реактивные переменные (кроме, конечно, id аттрибута — оно должно быть константой). Другим исключением являются свойства, начинающиеся на "!" — они должны быть функциями и отображаются на события элемента. Так же особого отношения требуют css свойства, они могут записываться двумя способами — как в примере выше:

    ["input-text", {
        "css/background": name.isSet().when(false, "red")
    }, name]

    или через создание отдельного объекта:

    ["input-text", {
        "css": {
            "background": name.isSet().when(false, "red")
        }
    }, name]

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

    Надеюсь теперь пример выше теперь стал очевиден. Давайте его усложним: мы захотели станного — для данного списка показать столько hello world'ов, сколько в нем элементов, и использовать в качестве значений по-умолчанию значения из этого списка. Раньше я говорил, что warp9 использует композицию как основной прием при проектировании интерфейсов, поэтому это не проблема. Добавляем новую функцию

    function NAMES(names){
        return ["div", names.lift(NAME)];
    }

    и заменяем

    warp9.ui.renderer.render(placeholder, NAME(new Cell("Denis")));

    на

    var names = new List();
    warp9.ui.renderer.render(placeholder, NAMES(names));
    names.add(new Cell("Denis"));
    names.add(new Cell("Lisa"));

    Мы увидили, как работают принципы композиции, но, помимо этого, warp9 предоставляет отличную поддержку модульности. Однако, при создании warp9 не было написано ни строчки кода, чтобы осознано этого добиться, по сути, модульность warp9 была открыта уже после того, как библиотека была написана. Дело в том, что представление в warp9 описывается кодом, из этого следует автоматическая поддержка модульности доступная для js файлов; получается, мы без проблем можем использовать, например, AMD для дистрибьюции отдельных контролов. В качестве иллюстрации я переписал предыдущий пример со списком имен с использованием AMD: само приложение и его исходники.

    Кастомные атрибуты


    Пока механизм кастомных атрибутов до конца не продуман и warp9 предоставляет несколько захардкоженных.

    !key:enter
    Событие элемента соответствующему тегу «input-text», возникает, когда пользователь нажимает enter.

    warp9:role и !warp9:changed
    Атрибуты элемента соответствующему тегу «input-check». Событие warp9:changed возникает, когда меняется состояние чекбокса, в аргументе передается текущее значение (true/false). Если warp9:role равен «view», то при изменении состояния чекбокса элемент не пытается изменить переданную реактивную переменную (но само состояние изменяется вслед за переменной).

    !warp9:draw
    Событие есть у каждого элемента, вызывается, когда элемент отрисовывается на экране.

    TodoMVC


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

    Заключение


    Сейчас у Warp9 нет версии, это работающий концепт, демонстрирующий:
    • Компонуемость как основной принцип построения UI
    • Модульность вплоть до дистрибьюции через requirejs
    • Бережливое отношение к утечкам памяти
    Этот набор мне кажется достаточно уникальный. Было бы здорого услышать разные мнения для доведения этого концепта до ума.
    Поделиться публикацией

    Комментарии 41

      +2
      Простите, но есть куча вспомогательных либ для AngularJS, чтобы сделать его реактивным (FRP) можно написать обертки на watcher, что дает возможность писать в FRP стиле. Вот одна из таких наработок github.com/lauripiispanen/angular-bacon и hueypetersen.com/posts/2013/06/26/helper-functions-for-rx-and-angular. Под промисы тоже много чего есть. Инфраструктура AngularJS очень большая, тем более сейчас разрабатывается такой модуль как github.com/jeffbcross/omnibinder.
        +1
        Ваш подход очень похож на Javelin.
          +1
          Я посмотрел на Javelin (https://github.com/tailrecursion/javelin). На первый взгляд он, так же как и RxJs и Bacon, не предоставляет ничего для постройки интерфейсов, только только дает возможность подцепиться к уже существующим элементам.

          Примеры на github.com/tailrecursion/javelin-demos тоже все в стиле: вот у нас DOM и мы к нему цепляемся и изменяем. По мне нет смысла городить FRP если конечным шагом является подписка на реактивную переменную и модификация DOM с помощью jquery из обработчика.

          Посмотрите на warp9 повнимательнее тут вся соль в композиции (собираем интерфейс по кусочкам) и модульности (любой кусок, вплоть до всего приложения включая view можно завернуть в AMD и загружать через requirejs). Дополнительным бонусом идет подход для сокращения утечек — посмотрите примеры, например, knockout и reactivecoffee этим страдают.
            0
            Кстати вскоре будет нативная поддержка PubSub. А пока у проекта Polymer есть прекрасный polyfill github.com/Polymer/observe-js
              +1
              Не совсем так, просто джавелин — это половина истории.

              github.com/tailrecursion/hoplon
                +4
                О, тогда Warp9 действительно похож на Javelin+Hoplon. Правда сложность инструментария меня убивает — нужно поставить Java, Clojure, Leiningen, Make, Boot, Hoplon, Javelin и выучить clojure чтобы написать hello world. Как-то уже не хочется проверять его на текучесть, но, подозреваю, что он тоже течет.
                  +1
                  Мне тяжело сказать, течëт ли он. В целом вполне возможно, ссылки на слушателей событий хранятся просто в свойстве watches у каждой ячейки.

                  На тему сложности — что с boot'ом, мне непонятно (в смысле почему они не используют просто лейнинген), но в целом всë проще. Нужна джава и шелл-скрипт для лейнингена, и практически все кложурные/кложурскриптовые проекты поедут. В случае с хоплоном еще нужен boot, но он ставится относительно проще. Clojure, Hoplon, Javelin — это всë библиотеки и они скачаются автоматически, так что на самом деле гемора не так и много.
          +2
          Очень тяжело читать, куча слипшихся слов «немногосодержательных», «целикомлежит » и т.д.
            +2
            Парсер хабра, постарался пофиксить.
              0
              спасибо, гараздо приятнее было дочитывать)
            +1
            Относительно сравнения с React:

            • headless — React позволяет ренерить UI на сервере. Он использует Virtual DOM вместо браузерного DOM для построения UI, из-за этого компоненты можно рендерить не только в бразуере, но и внутри Node.js (или любого другого js-рантайма) и Web Worker'а и для этого даже не потребуется бажный и медленный jsdom. Если интересно — можно посмотреть на react-app, набор утилит для рендеринга полностраничных (которые рендерятся прямо в document.documentElement) компонент c помощью React.
            • 2-way bindings — реализуется поверх в десяток строчек кода, но и этого не нужно делать, так как есть уже готовая утилита — ReactLink. Вообще, 2-way bindings имеют довольного ограничинное применение, формочки в основном, поэтому не думаю, что это очень важный пункт сравнения.
            • rich reactive list — абсолютно не важно для React, так как он работает с Plain Old JavaScript Objects, да-да, не стоит заморачиваться и моделировать данные с которыми вы работаете в каких-то observables и прочим.
            • template based — для React есть JSX, который позволяет описывать React компоненты привычным HTML синтаксисом, который имеют очень простую семантику компиляции, например <div>Hello</div> превращается в React.DOM.div(null, 'Hello')
              +1
              Да, React очень интересный, но мне он показался очень подробным, что-ли, например, в todomvc на нем в 3 раза (!) больше кода, чем в Warp9.

              Кроме того, у идея работать с POJSO есть свои минусы — если мы захотим по рабочему приложению получить слепок данных, чтобы их сохранить, например, — нам понадобится в каждый компонент добавить метод для экстракции этих данных из state. Посмотрите на исходники TodoMVC, там, чтобы этого избежать, пришлось все изменения делать внутри одного компонента (TodoApp), что тоже дурно попахивает, если мы говорим об архитектуре.
                0
                Кроме того, у идея работать с POJSO есть свои минусы — если мы захотим по рабочему приложению получить слепок данных, чтобы их сохранить, например, — нам понадобится в каждый компонент добавить метод для экстракции этих данных из state. Посмотрите на исходники TodoMVC, там, чтобы этого избежать, пришлось все изменения делать внутри одного компонента (TodoApp), что тоже дурно попахивает, если мы говорим об архитектуре.


                Чуть-чуть по-другому, то что весь state содержится в одном компоненте — это наоборот хорошо, а не плохо. Благодаря этому у нас всего один компонент, который содержит изменяющееся состояние, остальные без состояния и поэтому зависят только от своих аргументов (props в терминологии React).

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

                Хочу также опередить возможный контраргумент и уточнить, что в данном случае Single Responsibility Principle не нарушается — компонент отвечает только за один кусочек данных — список todo items.

                Ну и никто не мешает использовать React со всяким «сахаром» для моделей — FRP штуки, Backbone.Model и прочее.
                  +1
                  Проблема в том, что при росте приложения держать все состояние и логику работы с ней в одном компоненте проблематично.
                    0
                    Не понял аргумента вообще. Todo очень маленькое приложение и там раскидывать состояние в разные места я не вижу смысла. А в большом приложении вроде бы всë ок, у меня сверху спускаются какие-то общие данные (про пользователя, и все штуки, которые принадлежат ему), а всякое мелкое состояние для компонентов прямо в них определено. Общие данные сейчас бэкбоновые модели и в целом пока терпимо вроде, лучше, чем во всех остальных случаях было (кроме хоплона, ну и варп я не пробовал еще).
                      +1
                      Со спуском данных в компоненты (state) проблем нет, но если мы нашу иерархическую фигню, не только показываем и редактируем, но и сохраняем, то тут нужно писать дополнительный код в компонентах для экстракции данных из state.

                      Получается, что в небольших приложениях мы можем держать всю модель в state одного компонента (случай TodoMVC), а в больших — придется его распиливать и доставать. В этом нет ничего плохого, просто кода станет еще больше, а так React классный.
                        0
                        А, да, есть такое дело. Ты имеешь в виду, что в реакте нет внешнего состояния, которое можно было бы менять из внутренних компонентов? Да, иногда это несколько геморно, иногда плюс (меньше шансов слишком завязаться на глобальное состояние).
                          0
                          Я не копал глубоко React. А разве проблема держать модели и коллекции в виде конкретных полей state? Причем в плане MVC так даже болле логично, вью есть представление конкретной модели. Т.к. работаем со ссылками, экономим место и скорость. И собирать ничего не нужно, компонент возвращает модель с которой работает, при этом в приложении исчезают моменты когда происходит работа с какими либо промежуточными состояниями, только модели.
                            0
                            Хотел еще добавить, что часто именно модель содержит логику валидации, так что не придеться дублировать этот код. Это очень помогает когда абстракция приложения не подразумивает какие либо невалидные состояния.
                0
                Еще хотел спросить, что посоветуете прочитать для лучшего понимания работы монад?
                  0
                  Две вещи:
                  1. Если нету монады, то можно создать монаду
                  2. Если есть монада, то можно взять то, что внутри неё и создать из этого другую монаду


                  Теперь подставляем вместо слова «монада» конкретный тип данных, с которым мы работает — Maybe/Option, Promise/Future или что-то там ещё и забываем про монады. :p

                  P.S. Когда тут уже markdown сделают?
                  –2
                    +2
                    Вы, ваще-то, пробовали его запускать? Там нету чекбоксов, нету информации, сколько осталось задач, нету табов 'All', 'Active', 'Completed', нету удаления всех завершенных задач, нету редактирования и, подозреваю, что нету работы с local storage…
                    +1
                    Эта библиотека рассчитана для маленьких веб-приложений, где разработчик является верстальщиком и дизайнером, или нет?
                    Просто я не понял как быть, если мне верстальщик дает 5-и километровый HTML, который нужно «оживить».
                      +1
                      Не совсем. Я, скорее, думал про случаи, когда логика приложения сложнее, чем интерфейс. Возьмем, допустим gmail, врят ли при его разработке отталкивались от верстальщика. Вообщем, это библиотека для приложений посложнее CRUD.

                      Я, например, создал приложение для конфигурации топологии MongoDB кластера с помощью этой библиотеки, а там куча всяких правил, типа в одной шарде должно быть от 2 до 12 нод, только 7 могут голосовать, лучше намекать пользователю, когда он вручную назначает свойства canVote при числе нод меньше 7ми и так далее.
                      +2
                      логика в AngularJS отделена от шаблонов (в шаблонов, мы должны всегда помнить о логике (на лицо сайд эффект), но если так, то принцип разделяй и властвуй не работает, следовательно, композиция не дает нам преимуществ, следовательно ее нет.

                      Навскидку вижу 2 преимущества отделения логики от шаблонов:
                      * Разделение труда: над шаблоном работает один человек, над логикой — другой.
                      * Вы можете взять готовую директиву и подключить к своему шаблону (дизайну), т.е. сможете использовать готовые решения.
                        +2
                        Мне кажется, что эти два преимущества друг другу противоречат: при разработке директив активно смешиваются код и разметка, а если так, то где разделение труда?

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

                          Для директив AngularJS используйте templateUrl для указания пути к шаблону (https://github.com/angular-ui/bootstrap/tree/gh-pages тому пример)

                          В ваших примерах часто используется стилизация через атрибут style
                          ["div", {"css/margin-left": "10px"},
                          

                          ["input-text", {
                          	"css/background": hello.isSet().when(false, "red")
                          }, name],
                          

                          Как тоже самое сделать, только через классы? а то, что бы обновить дизайн страницы, простой верстальщик уже не подойдет.
                            +1
                            По поводу стилизации, да можно, но пока, к сожалению, можно добавить только один класс элементу. Это не фундаментальный недостаток, просто руки не дошли пока — большую часть разработки я боролся с утечками и несогласованностью событий. Пример с input-text можно переписать так:

                            ["input-text", {
                                "class": hello.isSet().when(false, "empty")
                            }, name]
                              +1
                              По поводу AngularJS. Нет, ну нельзя сказать, что мы используем templateUrl и проблем нет. Потому что в этом случае мы не имеем права говорить:
                              Вы можете взять готовую директиву и подключить к своему шаблону (дизайну), т.е. сможете использовать готовые решения.
                              Разница между подключением одного файла (js) и двух файлов (js и шаблон) огромна. В первом случае мы можем говорить про удобную дистрибьюцию, например, AMD, во втором нет — все делаем вручную. Кроме того, во втором случае перед нами открывается целый класс новых ошибок: что если один файл потеряется, что если файл шаблона я положу не туда куда нужно и так далее…
                                0
                                Боюсь, что будет сложно привязывать к дизайну готовые модули созданные на Warp9.
                                Придется переписывать модуль с учетом необходимого дизайна, причем не только css, но и разметку html блока.

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

                                P.S. Некоторые ругают css фреймворки за то, что сайты созданные с их помощью выглядят одинаково, а тут получается, что по разметке они будут похожи.
                              0
                              Ничего не смешивается, если не делать этого намеренно, про templateUrl уже упомянули, но так же есть следующие факты:
                              — Можно использовать директивы не только как отдельные элементы, но и как некоторое нагромождение функционала на элемент через restrict: 'A|E|C|M', то бишь мы можем, например, навесить несколько обработчиков клика как-то так:
                              <button click-watcher another-click-watcher>Click me</button>
                              

                              — Так же есть transclude, с помощью которого можно городить одну директиву внутри другой, в помощь ко всей этой вкуснятине приходит еще и наследование областей видимости ($scope которые), а так же возможность явно указать зависимость директивы от другой.
                            +1
                            Старнный у вас пример утечки для knockout… К нему легко можно придраться, сказав, что

                            this.handlers = ko.observable(0);
                            

                            нужно заменить на

                            this.handlers = ko.computed(function(){
                                        return this.items().length;
                                    }, this);
                            

                            Но вообще интересный аспект, не задумывался. Вот поддерживал бы JS слабые ссылки и проблемы такой не было бы.
                              +1
                              В смысле, странный? Я считаю то, что течет — подписки у переменной driver, если я начну считать что-то другое (кол-во элементов в списке), то memory leak от этого никуда не денется.
                                0
                                Ну, не странный, а не очевидный что-ли. Если посторонний человек зайдёт на страничку rystsov.github.io/warp9/pages/competitors/leaks/knockout.html то он не поймёт, не читая эту статью, о чём вообще идёт речь. Там не сделан акцент на то, что в this.driver() хранятся даже удалённые объекты nova.
                                  +2
                                  Ну ведь о том и речь, что он не очевидный. Кажется, что всë ок, а на самом деле…
                              0
                              А что получится в ситуации когда «b» вычисляется из «а», а потом вызвать «set» у «b»?
                                  var a = new Cell();
                                  var b = a.lift(function(a) {
                                      return a+2;
                                  });
                                  a.set(1);
                                  // b == 3
                              
                                  b.set(10);
                                  // b == ? a == ?
                              
                                0
                                Имхо, StopDoingWrongStuffException должен быть. :)
                                  0
                                  Знаю, что так делать нельзя. Интересно было, как на такое отреагирует библиотека.
                                  +1
                                  Я, кажется, в презентации это упоминал — у реактивный переменных, полученных иначе, чем через вызов конструктора, нет методов set/unset (если это Cell) и add/remove (если это List).
                                  0
                                  Как бы это всё к Dojo прикрутить? А то с виджетами куда приятнее работать чем с чистым домом…

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

                                  Самое читаемое