ngBindOnce

JavaScript performance comparison

Test case created by Josh Kurz

Info

What is the fastest implementation for bindonce directives?

This test is trying to prove what is the quickest way to set up bindings. It uses ngRepeat in every test, so the slowness of ngRepeat is negated. The only difference in each test is how the bindings are created.

Preparation code

<script src="http://code.angularjs.org/1.2.13/angular.js"></script>

<div id="myApp" ng-app="OneBinders" ng-controller="oneBindCtrl">
  <div bindonce ng-repeat="place in places">
    <a bo-href="place.src"><span bo-text="place.title"></span></a>
  </div>

  <div ng-repeat="place in places2">
    <a bb-one-bind-href="place.src"><span bb-one-bind-text="place.title"></span></a>
  </div>

  <div ng-repeat="place in places3">
    <a ng-href="{{place.src}}">{{place.title}}</a>
  </div>
</div>
 

<script>
var oneBinders = angular.module('OneBinders', ['pasvaz.bindonce'])
.controller('oneBindCtrl', function($scope, $rootScope){
       
      $rootScope.places = [];
      $rootScope.places2 = [];
      $rootScope.places3 = [];
     
});

oneBinders.service('BindOnceService', function($rootScope) {

  this.setTheValues = function(array,iterations){

      for(var i = 0;i < iterations;i++){
        $rootScope[array].push({title: 'Atlanta', src: 'http://upload.wikimedia.org/wikipedia/commons/a/a7/Atlanta_Skyline_from_Buckhead.jpg'});
        $rootScope[array].push({title: 'Los Angelas', src: 'http://www.wildnatureimages.com/images%202/060310-167..jpg'});
        $rootScope[array].push({title: 'New York', src: 'http://dormroomfund.com/img/slider-images/new-york-city.jpg'});
      }

  }

});

angular.forEach([{tag: 'Src', method: 'attr'}, {tag: 'Text', method: 'text'},
                 {tag: 'Href', method: 'attr'}, {tag: 'Class', method: 'addClass'},
                 {tag: 'Html', method: 'html'}, {tag: 'Alt', method: 'attr'},
                 {tag: 'Style', method: 'css'}, {tag: 'Value', method: 'attr'},
                 {tag: 'Id', method: 'attr'}, {tag: 'Title', method: 'attr'}], function(v){
    var directiveName = 'bbOneBind'+v.tag;
    oneBinders.directive(directiveName, function(){
        return {
            restrict: 'EA',
            link: function(scope, element, attrs){
                var rmWatcher = scope.$watch(attrs[directiveName], function(newV,oldV){
                    if(newV){
                        if(v.method === 'attr'){
                          element[v.method](v.tag.toLowerCase(),newV);
                        } else {
                          element[v.method](newV);
                        }
                        rmWatcher();
                    }
                });
            }
        };
    });
});

(function () {
  "use strict";
  /**
   * Bindonce - Zero watches binding for AngularJs
   * @version v0.3.1
   * @link https://github.com/Pasvaz/bindonce
   * @author Pasquale Vazzana <pasqualevazzana@gmail.com>
   * @license MIT License, http://www.opensource.org/licenses/MIT
   */


  var bindonceModule = angular.module('pasvaz.bindonce', []);

  bindonceModule.directive('bindonce', function ()
  {
    var toBoolean = function (value)
    {
      if (value && value.length !== 0)
      {
        var v = angular.lowercase("" + value);
        value = !(v === 'f' || v === '0' || v === 'false' || v === 'no' || v === 'n' || v === '[]');
      }
      else
      {
        value = false;
      }
      return value;
    };

    var msie = parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
    if (isNaN(msie))
    {
      msie = parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
    }

    var bindonceDirective =
    {
      restrict: "AM",
      controller: ['$scope', '$element', '$attrs', '$interpolate', function ($scope, $element, $attrs, $interpolate)
      {
        var showHideBinder = function (elm, attr, value)
        {
          var show = (attr === 'show') ? '' : 'none';
          var hide = (attr === 'hide') ? '' : 'none';
          elm.css('display', toBoolean(value) ? show : hide);
        };
        var classBinder = function (elm, value)
        {
          if (angular.isObject(value) && !angular.isArray(value))
          {
            var results = [];
            angular.forEach(value, function (value, index)
            {
              if (value) results.push(index);
            });
            value = results;
          }
          if (value)
          {
            elm.addClass(angular.isArray(value) ? value.join(' ') : value);
          }
        };
        var transclude = function (transcluder, scope)
        {
          transcluder.transclude(scope, function (clone)
          {
            var parent = transcluder.element.parent();
            var afterNode = transcluder.element && transcluder.element[transcluder.element.length - 1];
            var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
            var afterNextSibling = (afterNode && afterNode.nextSibling) || null;
            angular.forEach(clone, function (node)
            {
              parentNode.insertBefore(node, afterNextSibling);
            });
          });
        };

        var ctrl =
        {
          watcherRemover: undefined,
          binders: [],
          group: $attrs.boName,
          element: $element,
          ran: false,

          addBinder: function (binder)
          {
            this.binders.push(binder);

            // In case of late binding (when using the directive bo-name/bo-parent)
            // it happens only when you use nested bindonce, if the bo-children
            // are not dom children the linking can follow another order
            if (this.ran)
            {
              this.runBinders();
            }
          },

          setupWatcher: function (bindonceValue)
          {
            var that = this;
            this.watcherRemover = $scope.$watch(bindonceValue, function (newValue)
            {
              if (newValue === undefined) return;
              that.removeWatcher();
              that.checkBindonce(newValue);
            }, true);
          },

          checkBindonce: function (value)
          {
            var that = this, promise = (value.$promise) ? value.$promise.then : value.then;
            // since Angular 1.2 promises are no longer
            // undefined until they don't get resolved
            if (typeof promise === 'function')
            {
              promise(function ()
              {
                that.runBinders();
              });
            }
            else
            {
              that.runBinders();
            }
          },

          removeWatcher: function ()
          {
            if (this.watcherRemover !== undefined)
            {
              this.watcherRemover();
              this.watcherRemover = undefined;
            }
          },

          runBinders: function ()
          {
            while (this.binders.length > 0)
            {
              var binder = this.binders.shift();
              if (this.group && this.group != binder.group) continue;
              var value = binder.scope.$eval((binder.interpolate) ? $interpolate(binder.value) : binder.value);
              switch (binder.attr)
              {
                case 'boIf':
                  if (toBoolean(value))
                  {
                    transclude(binder, binder.scope.$new());
                  }
                  break;
                case 'boSwitch':
                  var selectedTranscludes, switchCtrl = binder.controller[0];
                  if ((selectedTranscludes = switchCtrl.cases['!' + value] || switchCtrl.cases['?']))
                  {
                    binder.scope.$eval(binder.attrs.change);
                    angular.forEach(selectedTranscludes, function (selectedTransclude)
                    {
                      transclude(selectedTransclude, binder.scope.$new());
                    });
                  }
                  break;
                case 'boSwitchWhen':
                  var ctrl = binder.controller[0];
                  ctrl.cases['!' + binder.attrs.boSwitchWhen] = (ctrl.cases['!' + binder.attrs.boSwitchWhen] || []);
                  ctrl.cases['!' + binder.attrs.boSwitchWhen].push({ transclude: binder.transclude, element: binder.element });
                  break;
                case 'boSwitchDefault':
                  var ctrl = binder.controller[0];
                  ctrl.cases['?'] = (ctrl.cases['?'] || []);
                  ctrl.cases['?'].push({ transclude: binder.transclude, element: binder.element });
                  break;
                case 'hide':
                case 'show':
                  showHideBinder(binder.element, binder.attr, value);
                  break;
                case 'class':
                  classBinder(binder.element, value);
                  break;
                case 'text':
                  binder.element.text(value);
                  break;
                case 'html':
                  binder.element.html(value);
                  break;
                case 'style':
                  binder.element.css(value);
                  break;
                case 'src':
                  binder.element.attr(binder.attr, value);
                  if (msie) binder.element.prop('src', value);
                  break;
                case 'attr':
                  angular.forEach(binder.attrs, function (attrValue, attrKey)
                  {
                    var newAttr, newValue;
                    if (attrKey.match(/^boAttr./) && binder.attrs[attrKey])
                    {
                      newAttr = attrKey.replace(/^boAttr/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
                      newValue = binder.scope.$eval(binder.attrs[attrKey]);
                      binder.element.attr(newAttr, newValue);
                    }
                  });
                  break;
                case 'href':
                case 'alt':
                case 'title':
                case 'id':
                case 'value':
                  binder.element.attr(binder.attr, value);
                  break;
              }
            }
            this.ran = true;
          }
        };

        return ctrl;
      }],

      link: function (scope, elm, attrs, bindonceController)
      {
        var value = attrs.bindonce && scope.$eval(attrs.bindonce);
        if (value !== undefined)
        {
          bindonceController.checkBindonce(value);
        }
        else
        {
          bindonceController.setupWatcher(attrs.bindonce);
          elm.bind("$destroy", bindonceController.removeWatcher);
        }
      }
    };

    return bindonceDirective;
  });

  angular.forEach(
  [
    { directiveName: 'boShow', attribute: 'show' },
    { directiveName: 'boHide', attribute: 'hide' },
    { directiveName: 'boClass', attribute: 'class' },
    { directiveName: 'boText', attribute: 'text' },
    { directiveName: 'boBind', attribute: 'text' },
    { directiveName: 'boHtml', attribute: 'html' },
    { directiveName: 'boSrcI', attribute: 'src', interpolate: true },
    { directiveName: 'boSrc', attribute: 'src' },
    { directiveName: 'boHrefI', attribute: 'href', interpolate: true },
    { directiveName: 'boHref', attribute: 'href' },
    { directiveName: 'boAlt', attribute: 'alt' },
    { directiveName: 'boTitle', attribute: 'title' },
    { directiveName: 'boId', attribute: 'id' },
    { directiveName: 'boStyle', attribute: 'style' },
    { directiveName: 'boValue', attribute: 'value' },
    { directiveName: 'boAttr', attribute: 'attr' },

    { directiveName: 'boIf', transclude: 'element', terminal: true, priority: 1000 },
    { directiveName: 'boSwitch', require: 'boSwitch', controller: function () { this.cases = {}; } },
    { directiveName: 'boSwitchWhen', transclude: 'element', priority: 800, require: '^boSwitch', },
    { directiveName: 'boSwitchDefault', transclude: 'element', priority: 800, require: '^boSwitch', }
  ],
  function (boDirective)
  {
    var childPriority = 200;
    return bindonceModule.directive(boDirective.directiveName, function ()
    {
      var bindonceDirective =
      {
        priority: boDirective.priority || childPriority,
        transclude: boDirective.transclude || false,
        terminal: boDirective.terminal || false,
        require: ['^bindonce'].concat(boDirective.require || []),
        controller: boDirective.controller,
        compile: function (tElement, tAttrs, transclude)
        {
          return function (scope, elm, attrs, controllers)
          {
            var bindonceController = controllers[0];
            var name = attrs.boParent;
            if (name && bindonceController.group !== name)
            {
              var element = bindonceController.element.parent();
              bindonceController = undefined;
              var parentValue;

              while (element[0].nodeType !== 9 && element.length)
              {
                if ((parentValue = element.data('$bindonceController'))
                  && parentValue.group === name)
                {
                  bindonceController = parentValue;
                  break;
                }
                element = element.parent();
              }
              if (!bindonceController)
              {
                throw new Error("No bindonce controller: " + name);
              }
            }

            bindonceController.addBinder(
            {
              element: elm,
              attr: boDirective.attribute || boDirective.directiveName,
              attrs: attrs,
              value: attrs[boDirective.directiveName],
              interpolate: boDirective.interpolate,
              group: name,
              transclude: transclude,
              controller: controllers.slice(1),
              scope: scope
            });
          };
        }
      };

      return bindonceDirective;
    });
  })
})();
</script>
<script>
Benchmark.prototype.setup = function() {
    var i = angular.element(document.getElementById('myApp')).injector();
    var s = i.get('BindOnceService');
    var scope = i.get('$rootScope');
};

Benchmark.prototype.teardown = function() {
    scope.places = [];
    scope.places2 = [];
    scope.places3 = [];
    scope.$apply();
};
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
https://github.com/Pasvaz/bindonce
s.setTheValues('places', 10);
scope.$apply();
pending…
https://github.com/angular/angular.js/pull/6284
s.setTheValues('places2', 10);
scope.$apply();
pending…
Regular Angular
s.setTheValues('places3', 10);
scope.$apply();
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment