34

When discussing the merits of AngularJS, two-way data binding is often touted as a major benefit of Angular over other JS frameworks. Digging deeper, the documentation suggests this process is done through dirty-checking rather than through event-driven measures. At first, it seems that the digest-loop works by having a method fire off in the background at periodic intervals, checking all the $watches during each cycle. However, reading further, it seems that the digest-loop is actually triggered by rootScope.digest(), which in turn is triggered by $.apply, which is in turn triggered by an event(!), such as an onClick event called through ng-click.

But, how can this be? I thought Angular doesn't use change listeners. So how does the digest-loop really operate? Does Angular automatically kick-off the digest-loop internally, or is the digest-loop triggered by events? If the digest-loop is run automatically, how often does it run?


Some clarification points:

  • I'm not asking about how the digest-loop runs when manually binding to changes. In this case, if you want to force a digest loop, you can do so by calling $.apply()
  • I'm also not asking about how often digest loop runs in response to user events. For example, if ng-model is on an input box, Angular will kick-off a digest loop when a user starts typing. The confusing part is that in order to know a user was typing, doesn't Angular use an event-based onKeyUp somewhere?
  • I already know that there is a limit of 10 cycles max per digest-loop. My question is less about the number of cycles per digest-loop, but rather the number of digest-loops that run, say, per second.
  • Bonus questions: How does the digest-loop relate to the JavaScript event-loop? Does the JS event loop run periodically in the background? Is the digest-loop the same thing as the event loop, but only in the "Angular Context"? Are these totally different ideas?
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
derekchen14
  • 725
  • 2
  • 7
  • 10

3 Answers3

13

Angular digests are triggered - they don't happen through polling.

Code executes, after code is done, angular triggers a digest.

Example:

 element.on('click', function() {
     $scope.$apply(function() { 
         // do some code here, after this, $digest cycle will be triggered
     });
 });

Angular will also trigger a $digest after the compile/link phase:

Compile > Link > Digest

And as to how many digest cycles are triggered? It depends on how soon the scope variables stabalise. Usually it takes at least 2 cycles to determine that.

Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • It sounds like there are two categories of event listeners then. (1) for changes on the model in memory, which is how Backbone & KO operate and (2) for changes based on user actions, which triggers a digest to perform dirty-checking, which is how Angular operates. Is that right? – derekchen14 Aug 13 '14 at 16:18
  • Angular has a mechanism for detecting changes to models in memory. It cycles through all your scope variables, if any have changed, it calls its corresponding $watch listener. When a user action happens, it triggers this cycle (ie. ng-click event). If you do anything in angular, angular will usually trigger the cycle - not because anything changed necessarily, but just-in-case anything changed. It does this a *lot*. You might think this is inefficient, and you are right it is. – Michael Kang Aug 13 '14 at 16:52
  • For example, when you make an ajax $http call, and later it returns, angular will trigger a $digest. Not because anything changed, or got triggered. It will do it anyway. If anything happened to change, it will trigger the $watch listener to update the view. This is quite different than how other frameworks work, whereby some event happens which triggers an event listener directly. In angular, it doesn't care if anything changed, it will trigger the $digest cycle anyway. – Michael Kang Aug 13 '14 at 16:55
12

Short direct answer to the main question is "NO", angular doesn't automatically trigger digest loop.

TL;DR answer:

Digest loop is designed to run dirty check over POJO models associated with Angular scope instances, so it only needs to run when a model might be changed. In a single page Web application running inside Browser, the following actions/events could lead to a model change

  1. DOM events
  2. XHR responses firing callbacks
  3. Browser's location changes
  4. Timers (setTimout, setInterval) firing the callbacks

Correspondingly, Angular trigger digest loop at, for instance

  1. input directives+ngModel, ngClick, ngMouseOver etc.
  2. $http and $resource
  3. $location
  4. $timeout

Try to answer those bonus questions from my understanding:

  1. ngModel directive often used with angular input directives (text, select etc) together, and the laters will listen on "change" events and call $setViewValue API exposed from ngModelController in order to sync back dom value. During the sync process, ngModelController will make sure to trigger digest loop.
  2. digest loop is different from JS event loop, the later is concept of JS runtime (checkout the great visualised session https://www.youtube.com/watch?v=8aGhZQkoFbQ) which run against event queue and remove consumed event from the queue automatically, but digest loop never remove watch from its watch list unless you explicitly unwatch.
  3. the number of digest loops per second depends on the efficiency of all watch callbacks being executed through the loop . If some bad code took one second to finish, then this digest loop would cost more than 1 sec.

So some key practices for avoid angular performance pitfalls are

  1. Watch callback should be coded as simpler/efficient as possible, for example detach complicated algorithm code to, for example, worker threads
  2. Proactively remove a watch if it is not used anymore
  3. Prefer to call $scope.$digest() instead of $scope.$apply() if applicable, $digest() only run part of scope tree and ensure the models associated under the subtree is reflecting to view. But $apply() will run against entire scope tree, it will iterate through more watches.
Roger Jin
  • 553
  • 5
  • 15
4

I believe this is what happens. AngularJS made a smart assumption that model changes happen only on user interaction. These interactions can happen due to

  • Mouse activity (move, clicked etc)
  • Keyboard activity (key up, key down etc)

AngularJS directives for the corresponding events wrap the expression execution in $scope.$apply as shown by @pixelbits in his example. This results in digest cycle.

There are some other events too where AngularJS triggers the digest loop. $timeout service and the $interval service are two such examples. Code wrapped in these service also results in digest loop to run. There maybe be some other events\services that can cause digest cycles to execute but these are the major ones.

This is the very reason that changes to model outside the Angular context does not update the watches and bindings. So one needs to explicitly call $scope.$apply. We do it all the time when integrating with jQuery plugins.

Chandermani
  • 42,589
  • 12
  • 85
  • 88