Practical ways to remove watches in an Angular 1.5 application
There have been a lot of articles written on the performance risks of using too many watches in angular 1. Obviously, an application needs some kind of change detection, or it will be nothing more than a static web page. Watches are one of the most common ways to do this in angular 1. However, I have found that in most cases, watches can be avoided. I'm going to explain a few techniques I've used to remove watches in my angular components.
All examples are in typescript. Where possible, I will include links with tips for how to accomplish the same thing in javascript. To follow along, view the source at https://github.com/SamGraber/RemovingWatchesAngular1.5/tree/Before
There are three different cases I have seen where a watch is used. In the first case, the directive is binding to a property in its template; perhaps an ng-model on an input, but it could be anything. The directive places a watch on this property so it can fire an event or notify some external service of changes. Second, a directive may place a watch on a property that is bound in via string ('@'), one-way ('<'), or two-way binding ('='), in order to update another local property or the template when a change is detected. Finally, a directive may set a watch on a property of an external service. We'll tackle these cases one at a time with strategies for how to remove the watches. Note that in addition to improving performance in our application, these changes also introduce change tracking techniques that will assist in a migration to angular 2, if we wish to do so in the future.
Case 1 – template property:
The first case is the easiest, since the controller has direct control over the property being used. To remove the watch, we simply need to wrap the property in a property declaration. In typescript, this is as easy as:
private _watchedValue: number;
get (): number {
return this._watchedValue;
}
set(value: number) {
this._watchedValue = value;
}
For using properties in plain javascript, see http://javascriptplayground.com/blog/2013/12/es5-getters-setters/
Once we have a property definition, we can move the watch code inside the set
block.
set(value: number) {
this._watchedValue = value;
this.doubledValue = this._watchedValue * 2;
}
We can now drop the $scope injection altogether and remove our dependency on the scope.
Full changes: https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/f2e4dfa87dd638a2cb49524d4dc5e54ecfede10d
Case 2 – watching a bound property
The second case involves removing a watch on a bound property, using a string ('@'), one-way ('<'), or two-way ('=') binding. To get started, we first want to convert all of the bindings we need to watch to one-way bindings, as these are the only type of binding that angular provides the ability to track changes for. For cases where this is not possible, we may have to leave the watches untouched at present.
A string binding can always be converted to a one-way binding, but note that this will require a change in the consumer, as the binding expression will now be 'parsed' rather than just 'interpolated'. http://www.programering.com/a/MDM5UjNwATk.html
Before:
// binding definition
string: '@',
// usage
string="A string"
string="{{localVar}}"
After:
// binding definition
string: '<',
// usage
string="'A string'"
string="localVar"
For a full example, see https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/3cdd449ca657b65169ff21093dfb1fb960dd73f3
For a two-way binding, we should be able to convert to a one-way binding by adding a corresponding event binding ('&'). This will take a bit more effort and a change in the consumer, but it is a better pattern moving forward. Instead of watching for changes to the bound object, angular will push changes through the event binding. This is also more compatible with the Angular 2 binding model. Note that if the two-way binding does not need to propagate changes from the child component, we can simply convert it to a one-way binding with no change.
Before:
// binding definition
value: '=',
// usage
value="localVar"
After
// binding definition
value: '<',
valueChange: '&',
// notify changes
this.valueChange({ value: this.value });
// usage
value="localVar" value-change="handleChange(value)"
For a full example, see https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/37744d98e6f032c88e7416a2ef4e73e105cdc296
Once all of our bindings are one-way, we can use a new lifecycle hook introduced in Angular 1.5.3: $onChanges. The $onChanges hook is called by angular whenever a change is detected in the one-way bindings with the previous, and new 'current' value of the binding. To remove the watch, we can simply check for changes to our binding every time the hook is called.
$onChanges(changes: any): void {
If (changes.binding) {
this.doubleValue = changes.binding.currentValue * 2;
}
}
For tips on how to specify typings for the change object, see the bonus section on specifying typing for $onChanges below.
We can now drop $scope from our controller dependencies.
Full change: https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/3fb45010bad9ff413932621d51c32cc2cb6c7f07
Case 3 – watching an external service
For the third case, we are watching for changes on an external service. This is the trickiest of the three, since there are no angular hooks we can rely on here. Being able to do so at all requires having some control over the contract of the external service. If you are using an external library, first check their documentation to see if they already have a mechanism for change detection. If not, you could contribute or submit an issue.
My favorite mechanism for pushing changes from a service is to provide an observable representing the change stream. We can use a property definition to push changes to the variable to the observable, similar to the first case.
private _watchedValue: number;
watchedValueObservable: Rx.Subject<number>;
constructor() {
this.watchedValueObservable = new Rx.Subject();
}
get(): number {
return this._watchedValue;
}
set(value: number) {
this._watchedValue = value;
this.watchedValueObservable.onNext(value);
}
Note – watchedValueChanges maybe be a good alternative name for this observable stream, as it mirrors angular 2 more closely.
In the consumer, we can simply replace the watch by subscribing to the stream.
this.watchedService.watchedValueObservable.subscribe((value: number): void => {
this.doubledValue = value * 2;
});
Full change: https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/b90f54a056f83a10c4fdec2a448a1d7b75c93520
After all code changes:
https://github.com/SamGraber/RemovingWatchesAngular1.5/tree/After
Bonus content
Typing for $onChanges
The $onChanges hook provides a change object representing each binding that has been updated. The binding object contains a 'currentValue' representing the changed value, as well as the 'previousValue'. We can build a generic interface to represent this 'change object', and then use this on a parent interface representing the changes we expect to see.
export interface IChangeObject<T> {
currentValue: T;
previousValue: T;
}
export interface IBindingChanges {
binding: IChangeObject<number>;
}
Full change: https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/c430aeec201f33e18a02915a7764be2cd10c6aef
Converting to component syntax
In addition to removing watches, in our application, we can also convert our directives to use the new component syntax introduced in angular 1.5 to make them more compatible with angular 2 changes. For a full description of the new component method, see https://toddmotto.com/exploring-the-angular-1-5-component-method/
Full Change: https://github.com/SamGraber/RemovingWatchesAngular1.5/commit/c27109322400ebaa876bbe93b0c28feb5492d35e
After bonus content changes: https://github.com/SamGraber/RemovingWatchesAngular1.5/tree/Bonus