4 minute read

TIL what the difference between angular’s watcher offerings are: $watch, $watchCollection, $watchGroup and $watch (objectEquality).

Angular watchers facilitate the 2 way data binding between views and controllers / services. Watchers bind listener functions to $scope properties (or expressions that evaluate to $scope properties). Bound $scope properties are dirty-checked during each $digest() cycle, and if a change in the property is detected the bound listener function is called.

There are a few different variations of watchers, each with different performance and purposes:

$watch

This is the most basic form of watcher. It accepts an expression as a string (either a $scope property or expression that evaluates to a $scope property) and a listener function to be called when the expression changes. This type of watcher only detects changes to shallow aspects of the bound $scope property (reference equality via === comparison). This means if the bound $scope property is an object, and a value in that object changes, the listener callback will not be called.

Example:

$scope.name = 'Brian'; // Initialize scope

$scope.$watch('name', function(new_val, old_val, scope) {
	console.log(new_val);
});

$scope.$digest(); // Value of $scope.name after $digest() is 'Brian'

// The following assignments would trigger the listener above.
// Note: between each assignment, the $scope would need to be $digest-ed for the change to be detected.
$scope.name = []; // A new array or object (a new reference)
$scope.name = undefined; // Making it undefined or null (which changes the reference)
$scope.name = 'hi'; // New strings
$scope.name = 1; // New primitives

// It will also be triggered when a new object with exactly the same values is passed (since reference is different).
$scope.name = ['Jim', 'John']; // triggers the watcher
$scope.name = ['Jim', 'John']; // triggers the watcher AGAIN since we created a new object (reference changed).

$watchCollection

This registers a watcher for an array or object that is bound to $scope. If any item within the collection changes, including the addition or removal of a new item, the listener is called. Equality is determined using === between cycles.

Example:

$scope.names = ['Brian', 'Alex', 'Sean'] // Initialize scope

$scope.$watchCollection('names', function(new_val, old_val, scope) {
	console.log(new_val);
});

$scope.$digest(); // Value of $scope.names after $digest() is ['Brian', 'Alex', 'Sean']


// The following assignments would trigger the listener above.
// Note: between each assignment below, the $scope would need to be $digest-ed for the change to be detected.
$scope.names = [] // A new array with different values
$scope.names = undefined // Changing reference to undefined
$scope.names.push('Corey') // Adding a new item to the collection
$scope.names.splice(0,1) // Removing an item from the collection
$scope.names[0] = 'Chris' // Changing a value at an index of the collection

// Note the difference between the first $watch and $watchCollection:
$scope.names = ['Jim', 'John']; // triggers the watcher
$scope.names = ['Jim', 'John']; // DOES NOT trigger the watcher again, even though it is a new object, since values are the same.

$watchGroup

The distinction between this watcher and $watchCollection, is that $watchGroup takes a group of expressions to watch instead of just one collection object. This can be useful if you need to bind a group of $scope properties to the same listener callback (instead of needing to write different watchers for them all). For performance reasons, $watchGroup delays calling the bound listener until the end of the $digest() cycle, to wait and see if multiple items in the group change during $digest() so the listener is only called once. $watchGroup is only available in angular 1.3 onwards, so we don’t use it yet in our application (still on angular 1.2.27).

$watch (objectEquality)

$watch can take a third boolean argument, which tells the watcher to use angular.equals to check for deep objectEquality of the bound item. This causes the watcher to fire even when a sub-key or value of an object inside of the watched $scope property changes.

Example:

$scope.full_name = [{'first_name':'Brian', 'last_name': 'Ambielli'}]; // Initialize scope

$scope.$watch('names', function(new_val, old_val, scope) {
	console.log(new_val);
}, true); // objectEquality boolean.

$scope.$digest() // Value of $scope.full_name after $digest() is [{'first_name':'Brian', 'last_name': 'Ambielli'}]

// This listener will be called for all situations outlined in the previous examples, including the following:
$scope.full_name[0].first_name = 'Lauren'; // changing a property of an object in the collection

// This watcher will NOT trigger its listener if a new object is assigned that is exactly the same
// as the original scope property, since angular.equals will return 'true' in this situation.
$scope.full_name = [{'first_name':'Michael', 'last_name': 'Ambielli'}] // Triggers the listener
$scope.full_name = [{'first_name':'Michael', 'last_name': 'Ambielli'}]; // WILL NOT trigger the listener

Because $watch (objectEquality) uses angular.equals for deep object comparison, it is the slowest performing of the watchers (deep comparison takes longer than shallow). For this reason, if you just need to watch a list of items and you don’t care about deep value changes in the list, $watchCollection or $watchGroup would better choices. Of course, if you just need to watch a single value (and shallow comparison will suffice) $watch without objectEquality is the speediest option.

Bonus TIL: I also learned that registering a watcher on scope returns a deregistration function that can be used to remove the watcher from the digest cycle if it is no longer needed.

Example:

var deregister = $scope.$watch('name', function(new_val, old_val, scope) {
	alert('watcher triggered!');
	deregister();
});

The next time $scope.name changes, the watcher will not be registered so the alert will not occur. This is a good way to tidy up after yourself and reduce the length of the digest cycle, if watchers only need to exist temporarily or under certain conditions. $watchCollection also returns a deregister function, so it can be used in the same way.

Updated:

Comments