US & Canada: 877 849 1850
International: +1 678 648 3113

Accelebrate Blog

ACCELERATED LEARNING, CELEBRATED RESULTS

Effective Strategies for Avoiding Watches in AngularJS

Watches in AngularJS are a powerful but easily abused approach to monitoring scope model changes. The scope in AngularJS is a DOM-linked prototype chain of objects descending from a root (top-level) scope object. These scope objects contain properties that are the application’s model data. Watches are an AngularJS feature that can be used to observe these models and execute code when the models change. Using a watch is not evil, but in many cases the same goal can be achieved with other techniques that do not carry the same performance penalty. Watches suffer a performance penalty because they must be re-evaluated with each run of the $digest loop. JavaScript is single threaded, therefore the digest loop blocks additional interaction between the user and the web page until the loop completes. When the value being observed by the watch changes, the function registered with the watch executes synchronously, locking the web browser user interface temporarily. For watches that run extremely quickly, the locking is not noticeable. However, for watches that do a significant amount of processing, a noticeable slowdown in an AngularJS application can occur.

Strategy One: $scope Communication

Scope CommunicationAngularJS scopes are organized in a hierarchical model following the DOM structure of the application. The top-level scope of an application is called the root scope. An entire tree structure of scopes descends from the root scope. Each child scope maintains a reference to its parent scope via the $parent property on the child scope. Also, child scopes can prototypally inherit from their parent scope or they can be isolated. Regardless of the state of inheritance, all scopes maintain a reference link to their parent scope in the hierarchy via the $parent property. Additionally, all scopes have “hidden” references to their children. Through these references, AngularJS is able to send messages up and down the tree of scopes. When a message is broadcast, it is transmitted from a scope to all of its children. When a message is emitted, it is transmitted from the scope up to its parents until reaching the root scope. Using a broadcast mechanism, a child scope can listen for a message from a parent scope instead of watching a particular model on the parent scope (either through the prototype chain or the $parent property). When the model on the parent (or any ancestor) scope is changed, that scope could send a message notifying all of the children that a change occurred. The child could then handle the change. The advantage to this method is that watches would not have to be configured and evaluated when the $digest loop runs in the child scopes for other more local scope updates. One disadvantage of this method is that scope communication via broadcast (and emit) is synchronous as well. So, if the code waiting for the message performs many operations, it too can slow down the process. One solution to this would be to execute the code that handles the message asynchronously to allow the message transmission to continue down the tree.

<div ng-app="MyApp">
  <div ng-controller="ParentCtrl">
    <input ng-model="myModel.myProp" ng-keyup="myPropChanged()" ng-keydown="myPropBeforeChange()">
    <div ng-controller="ChildCtrl">
       My Prop Old Value: {{myPropOldValue}}<br>
       My Prop New Value: {{myPropNewValue}}
    </div>
  </div>
</div>
 
<script>
  "use strict";
  angular.module("MyApp", [])
  .controller("ParentCtrl", function($scope) {
    $scope.myModel = {
      myProp: "Test Value",
    };
    var myPropOldValue = $scope.myModel.myProp;
    $scope.myPropBeforeChange = function() {
      myPropOldValue = $scope.myModel.myProp;
    };
    $scope.myPropChanged = function() {
      $scope.$broadcast("myPropChanged", { newValue: $scope.myModel.myProp, oldValue: myPropOldValue });
    };
  })
  .controller("ChildCtrl", function($scope) {
    $scope.$on("myPropChanged", function(event, options) {
      $scope.myPropOldValue = options.oldValue;
      $scope.myPropNewValue = options.newValue;
    });
  });
</script>

Strategy Two: ngModel $parsers

A second strategy involves a scenario when a scope model property is modified by a control that is decorated with ngModel. When the input control modifies the scope model property it does so through a series of parser functions that are executed in sequence. Typically, parser functions are used to validate the user entered data or format the data for storage on the scope model. However, it is possible to wire up parsers that produce the side effect of notifying some other part of the AngularJS application that the particular scope model property was modified. Arguably, such a side-effect would probably not be the best design choice; nevertheless, such an approach would have better performance than evaluating and executing a watch. The parser side effect would need to be configured as the last parser in the array of parsers so that the final value being updates on the scope matches the value being passed to the code observing the value. The advantage of this approach is that notification of the scope model property change happens the moment the change is made – there is no delay. The biggest downside to this approach is that it depends upon a side-effect.

<div ng-app="MyApp">
  <div ng-controller="MyCtrl">
    <input ng-model="myModel.myProp" notify-me="sendMeUpdateDir">
    <br>
    <input ng-model="myModel.myProp2" notify-me="sendMeUpdateDir2">
  </div>
</div>
 
<script>
  "use strict";
  angular.module("MyApp", [])
  .controller("MyCtrl", function($scope, $filter) {
    $scope.myModel = {
      myProp: "Test Value",
      myProp2: "Test Value 2"
    };
    $scope.sendMeUpdateDir = function(newValue, oldValue) {
      console.log("dir myProp value changed, new:" + newValue + ", old: " + oldValue);
    };
    $scope.sendMeUpdateDir2 = function(newValue, oldValue) {
      console.log("dir myProp2 value changed, new:" + newValue + ", old: " + oldValue);
    };
  })
  .directive("notifyMe", function() {
    return {
      scope:{
        notifyMe: "&"
      },
      require: "ngModel",
      link: function(scope, element, attrs, ctrl) {
        var oldValue;
        ctrl.$formatters.push(function(value) {
          oldValue = value;
          return value;
        });
        ctrl.$parsers.push(function(value) {
          scope.notifyMe()(value, oldValue);
          oldValue = value;
          return value;
        });
      }
    };
  });
</script>

Strategy Three: ngModel $viewChangeListeners

Another strategy requiring the ngModel controller allows custom directives to setup functions on an array named $viewChangeListeners. The array is a property on the ngModel controller and it will execute the functions in the array when the scope model property associated with the directive is changed. The view change listener functions are executed after the parser functions have been executed and the model value has been updated. Because neither the old nor new value is passed into the listener functions, but through the ngModel controller object, only the new view value can be referenced.

<div ng-app="MyApp">
  <div ng-controller="MyCtrl">
    <input ng-model="myModel.myProp" notify-me2="sendMeUpdateDir">
    <br>
    <input ng-model="myModel.myProp2" notify-me2="sendMeUpdateDir2">
  </div>
</div>
 
<script>
  "use strict";
  angular.module("MyApp", [])
  .controller("MyCtrl", function($scope, $filter) {
    $scope.myModel = {
      myProp: "Test Value",
      myProp2: "Test Value 2"
    };
    $scope.sendMeUpdateDir = function(newValue, oldValue) {
      console.log("dir myProp value changed, new:" + newValue + ", old: " + oldValue);
    };
    $scope.sendMeUpdateDir2 = function(newValue, oldValue) {
      console.log("dir myProp2 value changed, new:" + newValue + ", old: " + oldValue);
    };
  })
  .directive("notifyMe2", function() {
    return {
      scope:{
        notifyMe: "&"
      },
      require: "ngModel",
      link: function(scope, element, attrs, ctrl) {
        var oldValue;
        ctrl.$formatters.push(function(value) {
          oldValue = value;
          return value;
        });
        ctrl.$viewChangeListeners.push(function() {
          console.log("old value: " + oldValue);
          console.log("new value: " + ctrl.$modelValue);
          oldValue = ctrl.$modelValue;
        });
      }
    };
  });
</script>

Strategy Four: setInterval

A fourth strategy would be to configure an interval on the global object (in the case of a web browser, the window object). This would not be the $interval service since this would force the execution of the $digest loop. This would be the ordinary interval provided through the global object’s setInterval method. The setInterval method could check a scope model property on a specified interval for changes. When a change occurs, it could fire off a registered callback function with an apply function to execute the observer code within the context of the scope. The primary advantage to this approach is that it does not significantly interfere with the web browser user interface thread at all (except to compare the old and new value) unless there is a change to the value. One disadvantage to this approach is that there could be a slight delay between the time the scope model property is changed and the next time the interval function fires.

<div ng-app="MyApp">
  <div ng-controller="MyCtrl">
    <input ng-model="myModel.myProp">
    <br>
    <input ng-model="myModel.myProp2">
  </div>
</div>
 
<script>
  "use strict";
  angular.notifyMe = function(scope, expr, callbackFn) {
    var oldValue = scope.$eval(expr);
    setInterval(function() {
      var newValue = scope.$eval(expr);
      if (newValue !== oldValue) {
        setTimeout(function() {
          callbackFn.call(null, newValue, oldValue);
          oldValue = newValue;
        },0);
      }
    }, 100);
  };
  angular.module("MyApp", [])
  .controller("MyCtrl", function($scope, $filter) {
    $scope.myModel = {
      myProp: "Test Value",
      myProp2: "Test Value 2"
    };
    angular.notifyMe($scope, "myModel.myProp", function(newValue, oldValue) {
      console.log("int myProp value changed, new:" + newValue + ", old: " + oldValue);
    });
    angular.notifyMe($scope, "myModel.myProp2", function(newValue, oldValue) {
      console.log("int myProp2 value changed, new:" + newValue + ", old: " + oldValue);
    });
  });
</script>

Strategy Five: filters

A fifth strategy involves leveraging a filter to execute code when the value on which the filter is applied changes. When the value changes, the filter executes; therefore, the filter can be used to trigger a side-effect to avoid the use of a watch. The primary advantage of this approach is that it can evaluate both scope model properties and interpolated expressions. Additionally, it would only execute the filter function if a change was made. The primary disadvantage is that a filter is being used for a side-effect, and the filter will be in the mark up of the template which could make the template appear more complicated or confusing.

<div ng-app="MyApp">
  <div ng-controller="MyCtrl">
    <input ng-model="myModel.myProp">{{myModel.myProp | notifyMe:sendMeUpdateFil:'myProp'}}
    <br>
    <input ng-model="myModel.myProp2">{{myModel.myProp2 | notifyMe:sendMeUpdateFil2:'myProp2'}}
  </div>
</div>
 
<script>
  "use strict";
  angular.module("MyApp", [])
  .controller("MyCtrl", function($scope, $filter) {
    $scope.myModel = {
      myProp: "Test Value",
      myProp2: "Test Value 2"
    };
    $scope.sendMeUpdateFil = function(newValue, oldValue) {
      console.log("fil myProp value changed, new:" + newValue + ", old: " + oldValue);
    };
    $scope.sendMeUpdateFil2 = function(newValue, oldValue) {
      console.log("fil myProp2 value changed, new:" + newValue + ", old: " + oldValue);
    };
  })
  .filter("notifyMe", function() {
    var oldValues = [];
    return function(value, callbackFn, oldValueCacheKey) {
      if (value !== oldValues[oldValueCacheKey]) {
        callbackFn(value, oldValues[oldValueCacheKey]);
        oldValues[oldValueCacheKey] = value;
      }
      return value;
    };
  });
</script>

A Final Word

While watches are powerful, there are some good alternatives to not using a watch. If a watch must be used, then the watch expression should be as specific as possible. The comparison should be shallow and the same expression should not be watched by multiple watches. Finally, watches should be limited only to updating critical user interface elements since the digest loop in which they run is tied to the user interface thread. Avoid using watches as much as possible. When they are used, be sure to make them as efficient and limited as possible.

Download code examples


 

Author: Eric Greene, one of Accelebrate’s instructors.

Accelebrate offers private AngularJS training and JavaScript training for groups and instructor-led online JavaScript classes for individuals.

Categories: JavaScript Articles
Tags:

6 Responses to "Effective Strategies for Avoiding Watches in AngularJS"

Leave a Reply

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

*



You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Please contact us for GSA pricing.
Contract #GS-35F-0307T

Please see our complete list of
Microsoft Official Courses