I have had the incredible good fortune of being part of some very talented developers ramping up on AngularJS. It is inevitable that questions surrounding scope life-cycle work their way into the process. These usually come in the form of “But how does AngularJS know when this value has changed so that it can update bindings?” or “Hey I am listening for this DOM event in my directive and when I update scope, nothing is happening… what is the deal?” or even “Oh my goodness! How does it do that!? IS IT MAGIC!?”
At the core of AngularJS is the scope life-cycle and so I wanted to take this opportunity to elaborate on some of the fundamental aspects of how scope works in AngularJS. I have tried to distill this concept down to the most important bits so that a new developer could grasp scope life-cycle in less than 10 minutes and experienced developers could explain it in less than 10 minutes.
First Things First
Misko Hevery wrote a brilliant explanation of dirty checking vs change listeners on Stack Overflow that I think should be required reading for anyone coming into AngularJS. I hereby vote that it be included in the AngularJS docs IMMEDIATELY!
http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933
I want to point out a few things as it relates to what I want to talk about here:
- AngularJS compares a value with its previous value and if it has changed then a change event is fired. This is dirty checking in a nutshell.
- In AngularJS, a digest is the cycle that performs dirty checking. This is initiated via $digest().
- If something happens outside of AngularJS, you need to let AngularJS know to execute a digest cycle and you do that via $apply which calls $digest.
Now on to the nuts and bolts! I think that the scope life-cycle IS dirty checking and all the broad strokes can be summarized in $digest, watchExpressions and $apply.
$digest
This is the “heartbeat” of an AngularJS application. $digest processes all the watchExpressions for the current scope and its children.
So what happens when a watchExpression is processed? I will get into that a bit more when I talk about watchExpressions, but basically the value of the current watchExpression is compared with the value of the previous watchExpression, and if they do not match then it is “dirty” and a listener is fired.
With that said, you should not call $digest directly but call $apply which will force a $digest cycle. I will get more into the reasoning behind this in just a moment.
watchExpressions
Behind every $digest cycle is a good watchExpression. watchExpressions return the value of what is being watched on every $digest cycle. This is the trenches of dirty checking. If the value of the current watchExpression call and the previous watchExpression call do not match then a listener is fired.
Most watchExpressions are created implicitly within AngularJS, but you can also create them manually. Let me show you how to do that:
scope.$watch('name', function(newValue, oldValue) { /* Do something clever here */ });
Most of the time a watchExpression is just the name of a property you want to watch. In this example the watchExpression is “name”. If “name” changes then the listener function is called.
angular.equals is used to determine the equality of the two expressions while angular.copy is used to store the value of current value of a watchExpression for later comparison.
It is also important to understand that when one listener fires, it may change the model which will fire other listeners and so the watchers keep re-running until no more watchers are firing.
$apply
I like to think of $apply as a courier service that the outside world can recruit to tell AngularJS that something has happened and drop off an optional expression to be handled.
I mentioned previously that you use $apply to force a $digest cycle, but that is just one part of what happens with $apply. To follow the courier analogy, $apply hand delivers the message and possibly a package to the sweet lady at the front desk named $evalerie, but you can call her $eval. GROAN! Okay! Back on track! $eval checks to make sure that $apply dropped off a legitimate expression. If something is wrong while it is being evaluated then an exception is thrown and passed to the $exceptionHandler service. From there, $digest is called to kick off the $digest cycle.
Application level error handling is a nice feature that I highly recommend you utilize, especially as your application grows in complexity. The fact that we get it for free with $apply is really cool and why you should use it over $digest.
So when do you use $apply?
Good question! One of the main reasons you use $apply is when browser DOM events have triggered outside of AngularJS. jQuery I’m looking at you!
You may also need to use it for setTimeout events, XHR callbacks, and integrating with 3rd party libraries.
Here is a fiddle I put together to illustrate a use of $apply
http://jsfiddle.net/simpulton/huKHQ/
I freely admit that this example is a bit contrived – it stems from a real question my buddy Fritz had a few days ago.
The part I want to draw your attention to is in the code below:
$('.testButtonWithApply').on('click', function(){ $scope.$apply(function() { $scope.handleClick('Scope apply'); }); }); $('.testButtonWithoutApply').on('click', function(){ $scope.handleClick('This will not work'); });
Both functions are listening for events via jQuery and then making a request back into AngularJS from the event handler. The difference between the two is that I have wrapped the handleClick function in a function expression that I pass to $apply. I could have just called handleClick and THEN $apply but it is better to use the function expression so you get error handling.
More than once calling $apply has been the difference between me getting the behavior I expected, and crickets as nothing happened at all.
Conclusion
This concludes my primer on AngularJS scope life-cycle and what is behind the “magic” that blew me away when I started digging into AngularJS.
So to summarize, there is an internal cycle called $digest that runs through the application and executes watch expressions and compares the value returned with the previous value and if the values do not match then a listener is fired. This $digest cycle keeps looping until no more listeners are fired. You can call $digest directly, but it is recommended that you call $apply because it wraps a $digest call with an error handling mechanism. You call $apply when something outside of AngularJS happens that you need to notify AngularJS about. Well, I guess when you say it like that… it doesn’t seem so hard. Cheers!
Resources
The Misko Hevery Breakdown
http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933
$apply Example
http://jsfiddle.net/simpulton/huKHQ/
And be sure to check out the documentation. I have read it like ten times and am still learning things.
http://docs.angularjs.org/api/ng.$rootScope.Scope
Thank you!
Thanks Lukas for great notes on Angularjs scope life-cycle. I have one question, shouldn’t counter be scope.counter in “scope.$watch(‘name’, function(newValue, oldValue) { counter = counter + 1; })?”
Thanks Bilal! Actually the body of the function is arbitrary and so I had not put to much thought of what was in there. Code updated.
Good job Lukas! Here are some corrections/suggestions:
– “scope.$watch(‘name’, function() { /* Something clever goes here */ })”
it would be nicer to show the full signature of the $watch method
– “angular.equals is used to determine the equality of the two expressions”
we actually check identity via !== first and only if you call watch expression with 3rd argument set to true we use angular.equals
– $apply always calls $digest on the rootScope so the entire scope tree is dirty-checked, calling digest
on a non-root scope will dirty-check only the scope subtree that starts at the scope on which
$digest was called
Thanks so much for writing about AngularJS. This essence of what is required to encourage widespread adoption of this awesome framework.
Your fiddle helps me understand how it works!!! Thank you very much!!!
Please keep this blog up Lukas.. Very helpful.
Thank you for your excellent post!!
please continue!
I occasionally run into situations where the view unexpectedly does not get display model changes made in the controller. I don’t think I am running into the 10 digest cycle limit as the changes are not circular. If I try to run $apply() on the code in the controller that is changing the model scope that the view is dependent on I get an error that says that the an $apply is already in progress.
Error: $apply already in progress
at Error ()
…
It seems that the implicit watches that angular creates do not always get included in the digest cycle.
I looked at the code in the angular.js for the digest and the depth first search looks hackish.
Have you seen this behavior?
I found some help. Confirmed that I am not the only one that has seen this. It looks like it is dependent on what phase the digest cycle is currently on when you make the model change.
http://stackoverflow.com/questions/12463902/how-does-the-binding-and-digesting-work-in-angularjs#answer-12491335
http://www.yearofmoo.com/2012/08/use-angularjs-to-power-your-web-application.html
http://www.yearofmoo.com/2012/10/more-angularjs-magic-to-supercharge-your-webapp.html#apply-digest-and-phase
if(!$scope.$$phase) { //this is used to prevent an overlap of scope digestion
$scope.$apply(); //this will kickstart angular to recognize the change
}
Nice! Thanks for sharing!
Very well written and explained, Lukas. Thank you.
“Behind every $digest cycle is a good watchExpression”
I am not sure about this. What I have comprehended from the official docs is that each $digest cycle goes through a $watch list and thus potentially there could be more than one watchExpression.
You are correct. I was being humorous. I was not implying there was only one watchExpression but it is helpful to have at least ONE good watch watch expression 😀
Nice blog posts! I kept seeing this pattern (let’s see if this comment blows up):
$scope.$apply(function() {
$scope.handleClick(‘Scope apply’);
});
in other examples and now I get it. Thanks!
Hey everybody! It’s my buddy Aaron Hardy! High five!
Would the size of the data being watched cause performance issues. For example, data for a chart which could be quite large? Would it in this cause be better to have a flag being watched and then trigger the digest?
Hi Raff — that falls under the “it depends” category 😀 For instance… if the data were big enough it would cause performance issues on anything. I personally have never experienced performance problems with the $digest cycle even up to a thousand items. There are a few things you can do to work around that if becomes a problem such as paging the data or simplify the objects you are binding to.
Thanks. Its going to plug into the highcharts plugin and it may be very large data. I think I’ll just set a Boolean change flag when I receive the ajax payload so that Angular can see the data has changed without having to compare two large data-sets. Is that correct for this scenario or am I missing something?
As you say large data would cause performance issues on anything but as I am using Angular and want to build my component as a directive, I don’t want the digest cycle comparing extremely large data-sets. I suppose it comes down to whether I want it bound to a model.
Do you know the increment at which the Digest cycle runs?
Raff —
Digest cycle does not run on a predefined increment but rather uses dirty checking to know when the model has changed. This is similar to the invalidation validation cycle that Flex uses.
Have you built this out yet? I am curious what kind of performance you are seeing at the moment.
Thanks, excellent. I have created a directive and hooked some charts into sample data, but we are working on the services which will produce the final data so hard to tell. The rendering of multiple charts doesn’t cause any issues but if the data is updated regularly, it slows the page. This could well be to-do with Highcharts svg implementation which we are attempting to indentify. Early days yet but will report back with some code and a demo.
Really clear explanation of the $scope lifecycle. Love seeing your blog posts come up in google searches, because I know I’ll get not only the solution I’m looking for, but great foundational knowledge to go along with it. Thanks dude.
Thanks man! Totally made my day!
in the conclusion part, you mentioned “This $digest cycle keeps looping until no more listeners are fired”. So how does $digest cycle keeps looping? Is it so called “event loop?”, in which source-code of angular can I find the “keep looping?”
@yougen The $digest cycle keeps firing until the model stabilizes. This means that the $evalAsync queue is empty and no more changes are being detected by the $watch list.
You can read more about it here: http://docs.angularjs.org/guide/concepts#runtime
Exactly 10 minutes it took me to understand the cycle. Earlier, I was going back and forth between google and the angularjs documentation. This certainly is clear, concise and to the point.
Thanks again,
Bal
The only problem I have with digest cycle is when it is guaranteed to have your model change reflected on the view. Even calling $scope.$apply and $rootScope.apply doesn’t guarantee your application would run smoother without some cases when it work in some instances and throw inprog error in another instance. Very unpredictable.
This is mostly true but I have found this circumstance to either be a code smell or an extreme edge case. I would say it is ‘MOSTLY predictable’.
This is a great summary of the digest cycle. Working with third party libraries and maintaining angular.js performance can be tricky. I think knowing about watchers is important for angular.js development
Thanks, its a nice read.
This is by far the simplest explaination of digest cycle I have found. Thanks a ton!