ngBindOnce (v7)

Revision 7 of this benchmark created by Bernd Helzer on


Description

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 HTML

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0-rc.0/angular.min.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 ng-repeat="place in places4">
    <a once-href="place.src"><span once-text="place.title"></span></a>
  </div>

  <div ng-repeat="place in places5">
    <a set-href="place.src"><span set-text="place.title"></span></a>
  </div>

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

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

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;
    });
  })
})();


/**
 * angular-once - one time bindings for AngularJS
 * @version v0.1.7
 * @link https://github.com/tadeuszwojcik/angular-once
 * @author Tadeusz Wójcik <tadeuszwojcik@gmail.com>
 * @license WTFPL License, https://github.com/tadeuszwojcik/angular-once/blob/master/LICENSE.txt
 */

(function (window, angular, undefined) {
  'use strict';

  function setOneTimeBinding($scope, element, watch, watcherParser, bindingParser, done) {
    // get value to watch
    var watchingValue = watcherParser($scope);
    // if we have a valid value, render the binding's value
    if (watchingValue !== undefined) {
      // if watching and binding $parsers are the same, use watching's value, else $parse the new value
      return done(element, watcherParser == bindingParser ? watchingValue : bindingParser($scope));
    }
    
    // we do not have a valid value, so we register a $watch
    var watcherRemover = $scope.$watch(watch, function (newValue) {
      // wait until we have a valid value
      if (newValue == undefined) return;
      // remove this $watch
      removeWatcher();
      // if watching and binding $parsers are the same, use watching's value, else $parse the new value
      return done(element, watcherParser == bindingParser ? newValue : bindingParser($scope));
    });

    function removeWatcher() {
      if (watcherRemover) {
        watcherRemover();
      }
    }

    $scope.$on("$destroy", removeWatcher);
  }

  var once = angular.module('once', []);

  function makeBindingDirective(definition) {
    once.directive(definition.name, ['$parse', function ($parse) {
      return function ($scope, element, attrs) {
        var watch = attrs.onceWaitFor || attrs[definition.name];
        var watcherParser = $parse(watch);
        var bindingParser = attrs.onceWaitFor ? $parse(attrs[definition.name]) : watcherParser;
        setOneTimeBinding($scope, element, watch, watcherParser, bindingParser, definition.binding);
      };
    }]);
  }

  var bindingsDefinitions = [
    {
      name: 'onceText',
      binding: function (element, value) {
        element.text(value !== null ? value : "");
      }
    },
    {
      name: 'onceHtml',
      binding: function (element, value) {
        element.html(value);
      }
    },
    {
      name: 'onceSrc',
      binding: function (element, value) {
        element.attr('src', value);
      }
    },
    {
      name: 'onceHref',
      binding: function (element, value) {
        element.attr('href', value);
      }
    },
    {
      name: 'onceTitle',
      binding: function (element, value) {
        element.attr('title', value);
      }
    },
    {
      name: 'onceAlt',
      binding: function (element, value) {
        element.attr('alt', value);
      }
    },
    {
      name: 'onceId',
      binding: function (element, value) {
        element.attr('id', value);
      }
    },
    {
      name: 'onceIf',
      binding: function (element, value) {
        if (!value) {
          element.remove();
        }
      }
    },
    {
      name: 'onceClass',
      binding: function (element, value) {
        if (angular.isObject(value) && !angular.isArray(value)) {
          var results = [];
          angular.forEach(value, function (val, index) {
            if (val) results.push(index);
          });
          value = results;
        }
        if (value) {
          element.addClass(angular.isArray(value) ? value.join(' ') : value);
        }
      }
    },
    {
      name: 'onceStyle',
      binding: function (element, value) {
        element.css(value);
      }
    },
    {
      name: 'onceShow',
      binding: function (element, value) {
        if (value) {
          element.css('display', '');
        } else {
          element.css('display', 'none');
        }
      }
    },
    {
      name: 'onceHide',
      binding: function (element, value) {
        if (value) {
          element.css('display', 'none');
        } else {
          element.css('display', '');
        }
      }
    }
  ];

  angular.forEach(bindingsDefinitions, makeBindingDirective);

  once.directive('once', function () {
    return function ($scope, element, attrs) {
      angular.forEach(attrs, function (attr, attrName) {

        if (!/^onceAttr[A-Z]/.test(attrName)) return;
        var bind = function(element, value) {
          var dashedName = attrName.replace(/[A-Z]/g, function(match) { return '-' + match.toLowerCase(); });
          var name = dashedName.substr(10);

          element.attr(name, value);
        };

        setOneTimeBinding($scope, element, attrs, attrName, bind);
      });
    };
  });

})(window, window.angular);

(function () {
"use strict";

angular.module('watchFighters', [])

  .directive('setIf', [function () {
    return {
      transclude: 'element',
      priority: 1000,
      terminal: true,
      restrict: 'A',
      compile: function (element, attr, linker) {
        return function (scope, iterStartElement, attr) {
          iterStartElement[0].doNotMove = true;
          var expression = attr.setIf;
          var value = scope.$eval(expression);
          if (value) {
            linker(scope, function (clone) {
              iterStartElement.after(clone);
            });
          }
        };
      }
    };
  }])


  .directive('setHtml', function() {
    return {
      restrict: "A",
      priority: 100,
      link: function($scope, $el, $attr) {
        $($el).html($scope.$eval($attr.setHtml));
      }
    };
  })

  .directive('setText', function() {
    return {
      restrict: "A",
      priority: 100,
      link: function($scope, $el, $attr) {
        $($el).text($scope.$eval($attr.setText));
      }
    };
  })

  .directive('setClass', function() {
    return {
      restrict: "A",
      priority: 100,
      link: function($scope, $el, $attr) {
        var classVal = $scope.$eval($attr.setClass);
        if (angular.isObject(classVal)) {
          for (var key in classVal) {
            if (classVal.hasOwnProperty(key) && classVal[key]) {
              $el.addClass(key);
            }
          }
        } else {
          $el.addClass(classVal);
        }
      }
    };
  })

  .directive('setTitle', function() {
    return {
      restrict: "A",
      priority: 100,
      link: function($scope, $el, $attr) {
        $($el).attr('title', $scope.$eval($attr.setTitle));
      }
    };
  })

  .directive('setHref', function() {
    return {
      restrict: "A",
      priority: 100,
      link: function($scope, $el, $attr) {
        $($el).attr('href', $scope.$eval($attr.setHref));
      }
    };
  })

  ;

})();
</script>

Setup

var i = angular.element(document.getElementById('myApp')).injector();
    var s = i.get('BindOnceService');
    var scope = i.get('$rootScope');

Teardown


    scope.places = [];
    scope.places2 = [];
    scope.places3 = [];
    scope.places4 = [];
    scope.places5 = [];
    scope.places6 = [];
    scope.$apply();
  

Test runner

Ready to run.

Testing in
TestOps/sec
https://github.com/Pasvaz/bindonce
s.setTheValues('places', 100);
scope.$apply();
ready
https://github.com/angular/angular.js/pull/6284
s.setTheValues('places2', 100);
scope.$apply();
ready
Regular Angular
s.setTheValues('places3', 100);
scope.$apply();
ready
https://github.com/tadeuszwojcik/angular-once
s.setTheValues('places4', 100);
scope.$apply();
ready
https://github.com/abourget/abourget-angular
s.setTheValues('places5', 100);
scope.$apply();
ready
Angular 1.3 BindOnce
s.setTheValues('places6', 100);
scope.$apply();
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.