Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Will Belden
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Will Belden ( @CaptainPalapa )

Counting The Number Of Watchers In AngularJS - Updated For Isolate Scopes

By on

A few weeks ago, I blogged about a bookmarklet that could count the number of Scope.$watch() bindings in an AngularJS page. At the time, the bookmarklet didn't account for Isolate scopes since, frankly, I had very little experience with isolate scopes. This morning, however, I pushed a change to the bookmarklet project that now accounts for both standard and isolate scopes, even on the same element.

View this project on my GitHub account.

The approach I'm using in the bookmarklet is to query to the DOM (Document Object Model) for elements that have scope bindings. According to AngularJS, elements that have a normal scope binding will have the class "ng-scope"; and, elements that have an isolate scope binding will have the class "ng-isolate-scope". Since an element can have both a normal scope and an isolate scope, any given element may have none, one, or both of the aforementioned class names.

As of AngularJS 1.2, each wrapped element (jQuery or jQLite) exposes two .fn methods:

But, before AngularJS 1.2, there was only .scope(). Through trial and error, however, I have found out that a call to .scope() in AngularJS 1.0.8 will expose the watchers for both the normal scope and the isolate scope which means that we can still use the DOM-oriented approach to counting $$watchers across the different versions of AngularJS.

// I return the count of watchers on the current page.
function getWatchCount() {

	// Keep track of the total number of watch bindings on the page.
	var total = 0;

	// There are cases in which two different ng-scope markers will actually be referencing
	// the same scope, such as with transclusion into an existing scope (ie, cloning a node
	// and then linking it with an existing scope, not a new one). As such, we need to make
	// sure that we don't double-count scopes.
	var scopeIds = {};

	// AngularJS denotes new scopes in the HTML markup by appending the classes "ng-scope"
	// and "ng-isolate-scope" to appropriate elements. As such, rather than attempting to
	// navigate the hierarchical Scope tree, we can simply query the DOM for the individual
	// scopes. Then, we can pluck the watcher-count from each scope.
	// --
	// NOTE: Ordinarily, it would be a HUGE SIN for an AngularJS service to access the DOM
	// (Document Object Model). But, in this case, we're not really building a true AngularJS
	// service, so we can break the rules a bit.
	angular.forEach(
		document.querySelectorAll( ".ng-scope , .ng-isolate-scope" ),
		countWatchersInNode
	);

	return( total );


	// ---
	// PRIVATE METHODS.
	// ---


	// I count the $watchers in to the scopes (regular and isolate) associated with the given
	// element node, and add the count to the running total.
	function countWatchersInNode( node ) {

		// Get the current, wrapped element.
		var element = angular.element( node );

		// It seems that in earlier versions of AngularJS, the separation between the regular
		// scope and the isolate scope where not as strong. The element was flagged as having
		// an isolate scope (using the ng-isolate-scope class); but, there was no .isolateScope()
		// method before AngularJS 1.2. As such, in earlier versions of AngularJS, we have to
		// fall back to using the .scope() method for both regular and isolate scopes.
		if ( element.hasClass( "ng-isolate-scope" ) && element.isolateScope ) {

			countWatchersInScope( element.isolateScope() );

		}

		// This class denotes a non-isolate scope in later versions of AngularJS; but,
		// possibly an isolate-scope in earlier versions of AngularJS (1.0.8).
		if ( element.hasClass( "ng-scope" ) ) {

			countWatchersInScope( element.scope() );

		}

	}


	// I count the $$watchers in the given scope and add the count to the running total.
	function countWatchersInScope( scope ) {

		// Make sure we're not double-counting this scope.
		if ( scopeIds.hasOwnProperty( scope.$id ) ) {

			return;

		}

		scopeIds[ scope.$id ] = true;

		// The $$watchers value starts out as NULL until the first watcher is bound. As such,
		// the $$watchers collection may not exist yet on this scope.
		if ( scope.$$watchers ) {

			total += scope.$$watchers.length;

		}

	}

}

With all that said, it looks like this DOM-oriented approach is only possible because of a "debug mode" - which is enabled by default. This debug mode is intended to expose bindings through the DOM for consumption by 3rd-party scripts. According to the AngularJS 1.3 document, however, disabling this "debug mode" may have a significant performance boost in production, and would, of course, render my approach moot. I'll have to do some exploration into this debug mode.

Want to use code from this post? Check out the license.

Reader Comments

6 Comments

hi ben,

watcher function is greate, but we are using ui-router and after relasing 2.10 ui-router add "ng-scope" class to ui-view element. so it's make watcher count double :(( . u can check with "https://github.com/angular-ui/ui-router" sample. but its link to 2.08 version. so u need to change it before check counter.

can share your option on it??

15,666 Comments

@Tolga,

I don't know much about the UI-Router; but, as long as the scopes all have a unique ID, the scopes shouldn't be double-counted. If you look at the source code, it should check for "$id" values that have been visited:

if ( scopeIds.hasOwnProperty( scope.$id ) ) {
. . . . return;
}

As such, I don't know why it is double-counting anything. Is it possible that UI-Router is actually adding twice as many watchers with the latest version.

I'll take a look at their library and see if I can figure it out.

1 Comments

FYI, the function told me I had 54 watches which seemed way too low. I wrapped the getWatchCount() function in a timeout(for about 10 seconds) and I then had ~6000.

1 Comments

Curious if typing this in the console tells us enough? `angular.element(document.querySelector('body')).scope().$$watchersCount`

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel