Saturday, July 19, 2014

AngularJS Quick Tip: Counting Watches

Sometimes for troubleshooting purposes it is important to understand how many watches exist in an Angular app. You may be surprised to find that in certain scenarios, you are generating many more watches than you anticipate. For example, simply using ng-repeat on a long list will create a watch for every element unless you use a library like bindonce. Fortunately, because Angular is tightly coupled to the HTML DOM, it is not difficult to hook in and look under the hood.

To make it even easier, I created a simple module called jlikness.watch that contains a single directive and service. To use the module, you simply place a directive at the topmost element in your app you want to count watches from, then reference the service and call the countWatches() method any time you wish to get an updated count.

The main logic simply checks to see if an element has an associated scope, and if it does, uses Angular’s internal $$watchers collection to add the count.

if (element.scope()) {
    _this.watchCount += element.scope().$$watchers.length;
}

The function then recursively calls itself for each child of the element being inspected.

angular.forEach(element.children(), function (child) {
    iterate(angular.element(child));
});

This naive implementation will give a count, but because scope() walks up the DOM hierarchy to find the first parent with a valid scope the count may be off. The code can duplicate counts because children of the element may point to the same parent scope. The fix is to track the scopes by their Angular $id. You can see that code implemented in the latest source at the jlikness.watch repository.

The directive is used to pass the root element to the service as a starting point for the recursive count.

w.directive('jlWatch', ['jlWatchService', function (ws) {
    return {
        restrict: 'A',
        link: function (scope, elem) {
            ws.register(elem);   
        }
    };
}]);

Using the directive and service is very straightforward, as shown in this example. Click the refresh button to see the number of watches generated for even a fairly small app.

(function (app) {
    app.run(['$rootScope', 'jlWatchService', function ($rootScope, ws) {
        angular.extend($rootScope, {
            title: 'Angular Watchers',
            watchCount: 0,
            refreshWatches: function () {
                this.watchCount = ws.countWatches();
            }
        });       
    }]);
})(angular.module('myApp', ['jlikness.watch']));

That’s it – plain and simple, but very eye-opening when you start dealing with complex pages and long lists.