AngularJS vs. KnockoutJS

    Добрый день уважаемые, хабрачеловеки.
    В данной статье я хочу поделиться с вами своим опытом работы с такими фреймворками как AngularJS и Knockout.
    Cтатья будет интересна тем, кто хорошо знаком с JavaScript-ом и имеет представление хотя бы об одном из упомянутых фреймворков и естественно желает расширить свой кругозор.

    Обзор


    AngularJS и Knockout очень близки по своей идеологии. Они являются фреймворками для динамических веб-приложений и используют HTML в качестве шаблона. Они позволяют расширить синтаксис HTML для того, чтобы описать компоненты вашего приложения более ясно и лаконично. Из коробки они устраняют необходимость писать код, который раньше создавался для реализации связи model-view-controller.AngularJS и Knockout — это по сути то, чем HTML и JavaScript были бы, если бы они разрабатывались для создания современных веб-приложений. HTML — это прекрасный декларативный язык для статических документов. Но, к сожалению, в нем нет многого, что необходимо для создания современных веб-приложений.

    Features


    • Data-binding: простой и хороший способ связи UI и модели данных.
    • Мощный набор инструментов для разработчика (в частности у AngularJS, Knockout имеет достаточно бедный набор)
    • Легко расширяемый инструментарий


    Архитектура приложения


    Согласно документации, Angular предлагает структурировать приложение, разделяя его на модули. Каждый модуль состоит из:
    • функции, конфигурирующей модуль — она запускается сразу после загрузки модуля;
    • контроллера;
    • сервисов;
    • директив.
    Контроллер в понимании Angular — это функция, которая конструирует модель данных. Для создания модели используется сервис $scope, но о нем немного дальше. Директивы — это расширения для HTML.
    В свою очередь, Knockout предлагает строить приложение, разделяя его на ModelView, которые являются миксом из модели и контроллера. В пределах объекта ko.bindingHandlers размещены data-bindings, которые являются аналогами директив Angular. Для построения связи между моделью и ее представлением используются observable и observableArray.
    Говоря о модульности, нельзя не вспомнить про шаблон AMD — Asynchronous Module Definition. Angular и Knockout не имеют собственной реализации AMD шаблона. Советую использовать библиотеку RequireJS. Она себя очень хорошо зарекомендовала в плане совместимости и с Angular, и с Knockout. Больше интереcной информации о ней вы найдете тут: http://www.kendoui.com/blogs/teamblog/posts/13-05-08/requirejs-fundamentals.aspx и http://habrahabr.ru/post/152833/.

    Шаблонизация



    (Отдельная благодарность разработчикам AngularJS за такую прекрасную картинку)

    На данный момент уже существует огромное количество шаблонизаторов. К примеру, jQuery Templates (к сожалению, уже не поддерживается). Большинство из них работают по принципу: возьми статический template как string, смешай его с данными, создав новую строку, и полученную строку вставь в необходимый DOM-элемент посредством innerHTML свойства. Такой подход означает ререндеринг темплейта каждый раз после какого-либо изменения данных. В данном подходе существует ряд известных проблем, к примеру: чтение вводимых пользователем данных и соединение их с моделью, потеря пользовательских данных из-за их перезаписи, управление всем процессом обновления данных и/или представления. Кроме того, данный подход, на мой взгляд негативно сказывается на производительности.
    Angular и Knockout используют иной подход. А именно two-way binding. Отличительная особенность данного подхода — это создание двунаправленной связи элемента страницы с элементами модели. Такой подход позволяет получить достаточно стабильный DOM. В Knockout двунаправлення связь реализована посредством функций observable и observableArray. Для анализа шаблона используется HTML парсер jQuery (если подключен, в противном случае аналогичный родной парсер). Результатом работы упомянутых функций является функция, которая инкапсулирует текущее состояние элемента модели и отвечает за two-way binding. Данная реализация, на мой взгляд, не очень удобна поскольку возникает проблема связанная с копированием состояния модели: скоуп функции не копируется, поэтому необходимо сперва получить данные из элемента модели обратившись к нему, как к функции и только после этого клонировать результат.
    В Angular двунаправленная связь строится непосредственно компилятором (сервис $compile). Разработчику нет необходимости использовать функции подобные observable. На мой взгляд, это намного удобнее поскольку нет необходимости использовать дополнительные конструкции и не возникает проблемы при копировании состояния элемента модели.
    Ключевой же разницей в реализации шаблонизаторов в Angular и Knockout является способ рендеринга элементов: Angular генерирует DOM-элементы, которые потом использует; Knockout — генерирует строки и innerHTML-ит их. Поэтому генерация большого числа элементов занимает у Knockout больше времени (наглядный пример немного ниже).

    Модель данных


    Говоря о модели данных в Angular, обязательно стоит остановится на сервисе $scope. Этот сервис содержит ссылки на модель данных. Поскольку Angular предполагает наличие достаточно сложной архитектуры приложения, $scope также имеет более сложную структуру.
    Внутри каждого модуля создается новый экземпляр $scope, который является наследником $rootScope. Существует возможность програмно создать новый экземпляр $scope из существующего. В таком случае созданный экземпляр будет наследником того $scope, из которого он был создан. Разобратся с иерархией $scope в Angular не составит труда для тех, кто хорошо знает JavaScript. Такая возможность очень удобна, когда есть необходимость создания различных widgets, к примеру pop-ups.

    Data-binding


    Binding в Knockout, directive в Angular используются для расширения синтаксиса HTML, то есть для обучения браузера новым трюкам. Детально разбирать концепцию data-bindings и directives я не буду. Хочу лишь отметить, что data-binding это единственный в Knockout способ отображения данных и их связи с представлением.
    Более подробно данный вопрос рассмотрен в статьях:
    AngularJS: http://habrahabr.ru/post/164493/, http://habrahabr.ru/post/179755/, http://habrahabr.ru/post/180365/
    KnockoutJS: http://www.knockmeout.net/2011/07/another-look-at-custom-bindings-for.html
    Отдельно хочется упомянуть про наличие фильтров у Angular. Фильтры используются для форматирования выводимых на экран данных. К сожалению, Knockout для всего использует bindings.


    Примеры


    Fade-in анимация
    AngularJS: http://jsfiddle.net/yVEqU/
    var ocUtils = angular.module("ocUtils", []);
    ocUtils.directive('ocFadeIn', [function () {
        return {
            restrict: 'A',
            link: function(scope, element, attrs) {
                $(element).fadeIn("slow");
            }
        };
    }]);
    
    function MyCtrl($scope) {
        this.$scope = $scope;
        $scope.items = [];
        $scope.add = function () {
            $scope.items.push('new one');
        }
        $scope.pop = function () {
            $scope.items.pop();
        }
    }
    

    Knockout: http://jsfiddle.net/fH3TY/
    var MyViewModel = {
        items: ko.observableArray([]),
        fadeIn: function (element) {
            console.log(element);
            $(element[1]).fadeIn();
        },
        add: function () {
            this.items.push("fade me in aoutomatically");
        },
        pop: function () {
            this.items.pop();
        }
    };
    ko.applyBindings(MyViewModel, $("#knockout")['0']);
    

    Думаю, что проще этого примера будет сложно что-то найти, он отлично демонстрирует синтаксис фреймворков.

    Fade-out анимация
    AngularJS: http://jsfiddle.net/SGvej/
    var FADE_OUT_TIMEOUT = 500;
    var ocUtils = angular.module("ocUtils", []);
    ocUtils.directive('ocFadeOut', [function () {
        return {
            restrict: 'A',
            link: function(scope, element, attrs) {
                scope.$watch(attrs["ocFadeOut"],
                function (value) {
                    if (value) {
                         $(element).fadeOut(FADE_OUT_TIMEOUT);
                    }
                });
            }
        };
    }]);
    
    function MyCtrl($scope, $timeout) {
        this.$scope = $scope;
        $scope.items = [];
        $scope.add = function () {
            $scope.items.push({removed: false});
        }
        $scope.pop = function () {
            $scope.items[$scope.items.length - 1].removed = true;
            $timeout(function () {
                $scope.items.pop();
                console.log($scope.items.length);
            }, FADE_OUT_TIMEOUT);
        }
    }
    

    Knockout: http://jsfiddle.net/Bzb7f/1/
    var MyViewModel = {
        items: ko.observableArray([]),
        fadeOut: function (element) {
            console.log(element);
            if (element.nodeType === 3) {
                return;
            }
            $(element).fadeOut(function () {
                $(this).remove();
            });
        },
        add: function () {
            this.items.push("fade me in aoutomatically");
        },
        pop: function () {
            this.items.pop();
        }
    };
    ko.applyBindings(MyViewModel, $("#knockout")['0']);
    

    Данный пример не намного сложнее, чем предыдущий, но есть несколько нюансов.
    В случае с Angular, fadeOut должен быть выполнен до удаления элемента, поскольку DOM-елемнт связан с этим элементом модели и будет удален в тот же миг, когда будет удален элемент. Также важно отметить, что удаление элемента модели из массива стоит выполнять через сервис $timeout. Этот сервис по сути является оберткой для функции setTimeout и гарантирует целостность модели данных.
    У Knockout возникает проблема другого характера. Функция fadeOut получает в качестве первого аргумента массив DOM-элементов, относящихся к данному элементу модели. Иногда при странном стечении обстоятельств в процессе рендеринга шаблона могут быть созданы и соответственно они будут присутствовать в получаемом массиве, поэтому необходимо делать проверку элементов прежде чем выполнять fadeOut. Также по окончанию процесса fadeOut не забывайте удалять DOM-элементы (они не удаляются автоматически).

    Popup
    AngularJS: http://jsfiddle.net/vmuha/EvvY7/, http://angular-ui.github.io/bootstrap/ (по второй ссылке вы найдете достаточно много хороших и полезных решений)
    var ocUtils = angular.module("ocUtils", []);
    
    function MyCtrl($scope, $compile) {
        var me = this;
        this.$scope = $scope;
        $scope.open = function (data) {
            var popupScope = $scope.$new();
            popupScope.data = data;
            me.popup = $("<div class=\"popup\">{{data}}<br /><a href=\"#\" ng-click=\"close($event)\"> Close me</a></div>");
            $compile(me.popup)(popupScope);
            $("body").append(me.popup);
        }
        $scope.close = function () {
            if (me.popup) {
                me.popup.fadeOut(function () {
                    $(this).remove();
                });
            }
        }
    }
    

    Knockout: http://jsfiddle.net/vmuha/uwezZ/, http://jsfiddle.net/vmuha/HbVPp/
    var jQueryWidget = function(element, valueAccessor, name, constructor) {
        var options = ko.utils.unwrapObservable(valueAccessor());
        var $element = $(element);
        setTimeout(function() { constructor($element, options) }, 0);
        //$element.data(name, $widget);
    };
    
    ko.bindingHandlers.dialog = {
            init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
                console.log("init");
                jQueryWidget(element, valueAccessor, 'dialog', function($element, options) {
                    console.log("Creating dialog on "  + $element);
                    return $element.dialog(options);
                });
            }        
    };
        
    ko.bindingHandlers.dialogcmd = {
            init: function(element, valueAccessor, allBindingsAccessor, viewModel) {          
                $(element).button().click(function() {
                    var options = ko.utils.unwrapObservable(valueAccessor());
                    $('#' + options.id).dialog(options.cmd || 'open');
                });
            }        
    };
    
    var viewModel = {
        label: ko.observable('dialog test')
    };
    
    ko.applyBindings(viewModel);
    

    Реализовать popup можно по разному. Через директиву или байндинг и как часть ViewModel или модуля.
    В Angular для popup необходимо будет создавать новый экземпляр $scope, об этом я уже упоминал выше, и использовать сервис $compile для компиляции шаблона.
    В Knockout также скорей всего понадобится создание новой ModelView и вызова функции applyBindings для связи модели и представления.Думаю стоит заметить, что в случае, если для popup будет создана новая модель данных, то в Knockout возникнет проблема получения доступа к $rootModel из шаблона popup. Иерархия модели данных в Knockout построена на DOM-элементах, соответственно, если контейнер popup находится за пределами контейнера для приложения, то popup не будет иметь доступ к $rootModel.

    Форматирование цены
    AngularJS: http://jsfiddle.net/vmuha/k6ztB/1/
    Knockout: http://jsfiddle.net/vmuha/6yqDw/


    Производительность


    Перейдем к вопросу производительности. Были произведены 2 теста: холодный старт приложения “Hello World!” и рендеринг массива из 1000 элементов.
    На всех схемах по вертикали — миллисекунды, по горизонтали номер эксперимента.


    Здесь хорошо видно, что холодный старт у Knockout происходит на много быстрее, чем у Angular.


    А вот, когда речь заходит о рендеринге, здесь очевидно лидирует Angular. Как мы видим для рендеринга 1000 строк Knockout тратит до 2,5 секунд в то же время Angular хватает меньше 500 миллисекунд для выполнения этой задачи. Кроме того, отображение отрендеренных элементов на экране пользователя также занимает разное время: для Angular это 1-3 секунды, а для Knockout — 14-20 секунд. Это происходит из-за того что Knockout генерирует строки, а Angular — DOM-элементы.


    Резюме


    Самый главный вопрос для меня заключался в определении области применения Angular и Knockout. Проведя несколько простых экспериментов, я сделал следующие выводы:
    Knockout применим в случаях, когда нет необходимости в создании сложной архитектуры, сложных workflow-ов. Его основная функция — связь модели и представления, поэтому его лучше всего использовать для простых одностраничных приложений. К примеру, создание различного уровня сложности форм.
    Относительно Angular я пришел к выводу, что он будет полезен в тех случаях, когда требуется создание RichUI. Настоящего и полноценного one-page приложения со сложной архитектурой и сложными связями.


    P.S.:

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

    Подробнее
    Реклама

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

      +8
      *Картинка JPG vs PNG*

      Я бы еще добавил, что порог вхождения для AngularJS гораздо выше Knockout, особенно когда нужно реализовать вещи, не описанные в туториалах.
      А ваши выводы я поддерживаю, AngularJS — для сложных проектов, Knockout — для относительно простых.
        +2
        Помимо этого думаю стоит заметить, что экосистема у AngularJS куда более привлекательна (хотя скорость синхронизации изменений в коде и документации оставляет желать лучшего)
          +4
          Пишу сейчас проект на Angular. До этого с JS фреймворками дела не имел, писал исключительно backend для веба на Ruby(а до этого на PHP). До этого выбирал «на глаз» среди Backbone, Ember, Sails, Knockout: по наличию нормальных манов, видео туториалов и читаемости кода (субъективно, для неподготовленного к JS глазу).

          По поводу порога вхождения можно заметить только небольшие проблемы с документацией (даже на английском API reference довольно скудный), благо спасают ответы на похожие вопросы на StackOverflow. А в целом, Angular вполне дружелюбный и удобный в использовании.

          P.S. Начал знакомство с Angular с просмотра и конспектирования AngularJS Fundamentals In 60-ish Minutes (http://www.youtube.com/watch?v=i9MHigUZKEM) Видео охватывает основные принципы построения приложения и показывает как использовать тот же two-way binding и прочие полезные вещи.

          P.P.S. Также, для ознакомления, можно стянуть скелет для приложения на Angular: github.com/angular/angular-seed
            +2
            Лучше для ознакомления посмотреть вот этот скелет — github.com/angular-app/angular-app, там гораздо грамотнее и удобнее все сделано. На angular-seed как скелет совершенно не годится — слаборасширяем.
          +5
          gist.github.com/revolunet/4657146 про ангулар, куча демок и крутых приемов, из которых можно очень многому научиться
            +4
            Два момента:

            1) Количество задач, решаемых в Angular, сущесвенно больше: он дает вам структуру приложения, компонентизацию, раутинг, сервисы и т.д. Knockout предполагает, что для всего остального вы будете использовать другие инструменты. Например, Sammy JS для раутинга, Breeze JS для синхронизации данных с сервером, и т.д. В целом, выбор того или иного фреймворка — дело опыта и вкусовых предпочтений.

            2) Показывать график без кода самого бенчмарка смысла нет. Легким гуглением можно найти ссылки на JSPerf, в которых как и Knockout, так и Angular оказываются быстрее. Все зависит от кода. Например, jsperf.com/angular-vs-knockout-vs-ember/161 Если пощелкать разные версии, то то один, то другой фреймворк оказывается впереди. По всей видимости, проблема не дает покоя фанатам каждого из них. На практике все эти фреймворки близки по скорости, и стоит обращать внимание на производительность в контексте вашей задачи (страница тормозит — открыли профайлер).
              0
              Для тестов писался самый простой код. Можете ознакомится с ним по ссылке: www.ex.ua/528626323543
              Относительно фанатизма, к сожалению, я не успел записатся ни в один из фан-клубов ;)
              +5
              Есть один важный момент, databinding в Knockout использует change listeners поэтому необходимо: вызывать обертки типа ko.observable(), ko.computed, создавать custom bindings для нестандарных контролов.
              databinding в AngularJS основан на dirty-checking, это позволяет использовать любые объекты в качестве данных и изменять их свойства без необходимости использования сеттеров геттеров.
              При близком рассмотрении, Angular и Knockout очень различаются по своей идеологии и предназначению.
              0
              Пара замечаний относительно scope в Angular. Первое, утверждение
              Внутри каждого модуля создается новый экземпляр $scope, который является наследником $rootScope.
              является неверным. Новые экземпляры $scope обычно возникают при использовании директив (в случае если директива запрашивает создание нового scope), либо создаются вручную. Второе, на мой взгляд scope не является моделью данных. Модель данных в Angular — это обычные javascript объекты. Scope же является связующим звеном между ними и html-представлением (View). Наиболее похожее из того что мне встречалось — это ViewModel в .Net WPF.
                0
                Есть ещё проект Angular Light, похож на Angular.js с «элементами» Knockout.js.
                  0
                  Я правильно понимаю что он силен более низкоуровневым подходом?
                  +2
                  me.popup = $("<div class=\"popup\">{{data}}<br /><a href=\"#\" ng-click=\"close($event)\"> Close me</a></div>");
                  А без разметки в JS-коде никак нельзя обойтись малой кровью? У меня почему-то пятнистый блевонтон на такой подход.
                    0
                    Если бы речь шла о решении конкретной задачи, а не о примере использования, накиданного на скорую руку, то можно было бы.
                    0
                    Я чаще всего использую нокаут для добавления небольшого функционала в старые jQuery проекты. А новое начинаю на ангуляре писать, чтобы уже по полной его использовать, включая роутинг.
                      0
                      $scope это не модель! В $scope содержатся ссылки на модели данных, поправьте.
                        0
                        Спасибо, исправил.
                          0
                          «сожержит» :-)

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

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