Pull to refresh

AngularJS vs. KnockoutJS

Reading time 8 min
Views 57K
Добрый день уважаемые, хабрачеловеки.
В данной статье я хочу поделиться с вами своим опытом работы с такими фреймворками как 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.:

Надеюсь, данная статья будет всем интересна. Буду рад прочитать ваши комментарии, отзывы и конструктивную критику! Желаю всем приятной работы!
Tags:
Hubs:
+45
Comments 18
Comments Comments 18

Articles