Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with:

Scope $watch() vs. $watchCollection() In AngularJS

By Ben Nadel on

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>
  • &mdash;
  • <a ng-click="changeShallowValue()">Change Shallow Value</a>
  • &mdash;
  • <a ng-click="rebuild()">Rebuild</a>
  • &mdash;
  • <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.




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". :-(

Reply to this Comment

@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.

Reply to this Comment

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."

Reply to this Comment

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?

Reply to this Comment

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.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.