Effective Strategies for Avoiding Watches in AngularJS

November 11, 2014 in Web Development Articles

Written by Eric Greene


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


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


Written by Eric Greene

Eric Greene

Eric is a professional software developer specializing in HTML, CSS, and JavaScript technologies. He has been developing software and delivering training classes for nearly 19 years. He holds the MCSD Certification for ASP.Net Web Applications, and is a Microsoft Certified Trainer.


Learn faster

Our live, instructor-led lectures are far more effective than pre-recorded classes

Satisfaction guarantee

If your team is not 100% satisfied with your training, we do what's necessary to make it right

Learn online from anywhere

Whether you are at home or in the office, we make learning interactive and engaging

Multiple Payment Options

We accept check, ACH/EFT, major credit cards, and most purchase orders



Recent Training Locations

Alabama

Birmingham

Huntsville

Montgomery

Alaska

Anchorage

Arizona

Phoenix

Tucson

Arkansas

Fayetteville

Little Rock

California

Los Angeles

Oakland

Orange County

Sacramento

San Diego

San Francisco

San Jose

Colorado

Boulder

Colorado Springs

Denver

Connecticut

Hartford

DC

Washington

Florida

Fort Lauderdale

Jacksonville

Miami

Orlando

Tampa

Georgia

Atlanta

Augusta

Savannah

Hawaii

Honolulu

Idaho

Boise

Illinois

Chicago

Indiana

Indianapolis

Iowa

Cedar Rapids

Des Moines

Kansas

Wichita

Kentucky

Lexington

Louisville

Louisiana

New Orleans

Maine

Portland

Maryland

Annapolis

Baltimore

Frederick

Hagerstown

Massachusetts

Boston

Cambridge

Springfield

Michigan

Ann Arbor

Detroit

Grand Rapids

Minnesota

Minneapolis

Saint Paul

Mississippi

Jackson

Missouri

Kansas City

St. Louis

Nebraska

Lincoln

Omaha

Nevada

Las Vegas

Reno

New Jersey

Princeton

New Mexico

Albuquerque

New York

Albany

Buffalo

New York City

White Plains

North Carolina

Charlotte

Durham

Raleigh

Ohio

Akron

Canton

Cincinnati

Cleveland

Columbus

Dayton

Oklahoma

Oklahoma City

Tulsa

Oregon

Portland

Pennsylvania

Philadelphia

Pittsburgh

Rhode Island

Providence

South Carolina

Charleston

Columbia

Greenville

Tennessee

Knoxville

Memphis

Nashville

Texas

Austin

Dallas

El Paso

Houston

San Antonio

Utah

Salt Lake City

Virginia

Alexandria

Arlington

Norfolk

Richmond

Washington

Seattle

Tacoma

West Virginia

Charleston

Wisconsin

Madison

Milwaukee

Alberta

Calgary

Edmonton

British Columbia

Vancouver

Manitoba

Winnipeg

Nova Scotia

Halifax

Ontario

Ottawa

Toronto

Quebec

Montreal

Puerto Rico

San Juan