Scope $watch() vs. $watchCollection() In AngularJS
AngularJS has always had a Scope.$watch() function as means to observe [and react to] changes in a given value. With AngularJS 1.1.4, however, they added the Scope.$watchCollection() function as a means to observe changes in a collection (either as an Array or an Object). Between the two current functions, there are three unique ways to watch a value for changes. And to be honest, it can get a bit confusing. As such, I wanted to take a quick look at these three different watch-configurations and nail down what kind of changes each one will track.
Run this demo in my JavaScript Demos project on GitHub.
By default, the $watch() function only checks object reference equality. This means that within each $digest, AngularJS will check to see if the new and old values are the same "physical" object. This means that the vanilla $watch() statement will only invoke its handler if you actually change the underlying object reference.
The $watch() function takes a third, optional argument for "object equality." If you pass-in "true" for this argument, AngularJS will actually perform a deep-object-tree comparison. This means that within each $digest, AngularJS will check to see if the new and old values have the same structure (not just the same physical reference). This allows you to monitor a larger landscape; however, the deep object tree comparison is far more computationally expensive.
With AngularJS 1.1.4, the $watchCollection() function was added for collection-oriented change management. The $watchCollection() function is a sort-of mid-ground between the two $watch() configurations above. It's more in-depth than the vanilla $watch() function; but, it's not nearly as expensive as the deep-equality $watch() function. Like the $watch() function, the $watchCollection() works by comparing physical object references; however, unlike the $watch() function, the $watchCollection() goes one-level deep and performs an additional, shallow reference check of the top level items in the collection.
To see this in action, I've put together a demo that uses all three watch configurations to observe changes in a single array array. Then, I've provided several means to change the structure of the array. Each watch function tracks and logs the changes it observes.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Scope $watch() vs. $watchCollection() In AngularJS
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
</style>
</head>
<body ng-controller="AppController">
<h1>
Scope $watch() vs. $watchCollection() In AngularJS
</h1>
<p>
<a ng-click="changeDeepValue()">Change Deep Value</a>
—
<a ng-click="changeShallowValue()">Change Shallow Value</a>
—
<a ng-click="rebuild()">Rebuild</a>
—
<a ng-click="clear()">Clear</a>
</p>
<h2>
$watchCollection( collection ) Log
</h2>
<ul>
<li ng-repeat="item in watchCollectionLog">
{{ item }}
</li>
</ul>
<h2>
$watch( collection ) Log
</h2>
<ul>
<li ng-repeat="item in watchLog">
{{ item }}
</li>
</ul>
<h2>
$watch( collection, [ Equality = true ] ) Log
</h2>
<ul>
<li ng-repeat="item in watchEqualityLog">
{{ item }}
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// These are the log item to render upon change.
$scope.watchCollectionLog = [];
$scope.watchLog = [];
$scope.watchEqualityLog = [];
// I am the collection being watched.
$scope.collection = [
{
id: 1,
value: 0
}
];
// Use the relatively new watchCollection().
$scope.$watchCollection(
"collection",
function( newValue, oldValue ) {
addLogItem( $scope.watchCollectionLog );
}
);
// Use the old watch() with default object equality,
// which defaults to use object REFERENCE checks.
$scope.$watch(
"collection",
function( newValue, oldValue ) {
addLogItem( $scope.watchLog );
}
);
// Use the old watch() method, but turn on deep object
// equality, which will compare the deep object tree
// for changes.
$scope.$watch(
"collection",
function( newValue, oldValue ) {
addLogItem( $scope.watchEqualityLog );
},
true // Object equality (not just reference).
);
// ---
// PUBLIC METHODS.
// ---
// Change a deep value in an existing item on in the
// current collection.
$scope.changeDeepValue = function() {
// Add new item to collection.
$scope.collection[ 0 ].value = now();
};
// Add a new item to the collection, causing a change
// in the shallow topology of the collection.
$scope.changeShallowValue = function() {
// Add new item to collection.
$scope.collection.push({
id: ( $scope.collection.length + 1 ),
value: now()
});
};
// I clear the log items.
$scope.clear = function() {
$scope.watchCollectionLog = [];
$scope.watchLog = [];
$scope.watchEqualityLog = [];
};
// I rebuild the underlying collection, completely
// changing the reference.
$scope.rebuild = function() {
$scope.collection = [{
id: 1,
value: 0
}];
};
// ---
// PRIVATE METHODS.
// ---
// I add a log item to the beginning of the given log.
function addLogItem( log ) {
var logItem = (
"Executed: " + now() +
" ( length: " + $scope.collection.length + " )"
);
log.splice( 0, 0, logItem );
}
// I return the current UTC milliseconds.
function now() {
return( ( new Date() ).getTime() );
}
}
);
</script>
</body>
</html>
To frame this conversation, it's important to understand how often $digests run in AngularJS. They run a lot. Probably far more than you think or expect. As such, it's important to make your watchers as light weight as possible. And, understanding how the various watchers work is the first step in choosing the right one for your particular context.
Want to use code from this post? Check out the license.
Reader Comments
Wow, this has real "gotcha" implications. I'd have thought the $watch with equality flag would trigger on a rebuild and that the $watchCollection would trigger on deep values as well. And none of them seem to trigger on "any change anywhere". :-(
@Sharon,
It's definitely an interesting problem to tackle since watching a collection has got to be expensive. Definitely, when possible, your watches should be as small and as targeting as possible. That said, I've definitely had situations where I do need to watch a number of things since several sources may be updating a collection. But, I think the way I had things wired together was sub-optimal.
So am I wrong in saying the only real issue here is watching all of an objects properties is expensive if and only if we are dealing with Big Data?
So by this I mean that the main concern is scale its memory implications.
A solution I propose to watching very large data objects is to actually allow the directives to control when data is requested. (I know this goes against some common practices)
This could be done by just watching an array of url objects that the directive is associated with. If that array changes in any way then request new data. Since an array of url objects will always be small in comparison to the data it requests it is ok to look for changes and have logic in place to request relative data depending upon the change.
"Just thinking on a keyboard."
Thanks Ben, video really fast-tracked the learning process of understanding what each does.
Sam
I haven't completely read up on the differences between $watch and $watchCollection, but it seems to me like this new method could have actually been a restructuring of the initial $watch method to allow an integer rather than boolean for the third parameter, potentially allowing a specific number of depths within the object to perform comparison checks.
No long strictly a shallow or deep check, rather a custom option unique to each use case. Any thoughts?
Thanks. Perfect.
Hi Ben,
This article was useful to me. I have tried using watch in my project. I see all the three watches are called only once during initialization. In my case, the collection is a nested object. I have written my issue here:
http://stackoverflow.com/questions/24554121
Will you please point out where I am wrong.
thank you !
I don't know what you mean by deep and shallow. A visualization would be helpful.
Hi. thanks for the demo! A couple of things in your video are leaving me confused.
Around 2:33 and 2:43 I am not understanding your explanation about why the rebuild has a different effect the first time you click it after a clear. You give a "probable" explanation, as you put it, but when I look into the code, I can't actually see why...
I am also having a hard time understanding the way you are using the word physical. At times you seem to mean different things when you use the word. There must be some metaphor that you have in mind but I don't think I get it... maybe the more typical terms "by value" and "by reference" would come in handy?
thanks for the post, it helped me figure out some bugs in the code of an angular-slick directive. Will now make a fork
Thanks, nice explanation