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.
AngularJS 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>
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>
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>
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>
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>
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.
Accelebrate offers private AngularJS training and JavaScript training for groups and instructor-led online JavaScript classes for individuals.
Written by 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.
Our live, instructor-led lectures are far more effective than pre-recorded classes
If your team is not 100% satisfied with your training, we do what's necessary to make it right
Whether you are at home or in the office, we make learning interactive and engaging
We accept check, ACH/EFT, major credit cards, and most purchase orders
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