$scope.$evalAsync() vs. $timeout() In AngularJS
Sometimes, in an AngularJS application, you have to explicitly tell AngularJS when to initiate it's $digest() lifecycle (for dirty-data checking). This requirement is typically contained within a Directive; but, it may also be in an asynchronous Service. Most of the time, this can be easily accomplished with the $scope.$apply() method. However, some of the time, you have to defer the $apply() invocation because it may or may not conflict with an already-running $digest phase. In those cases, you can use the $timeout() service; but, I'm starting to think that the $scope.$evalAsync() method is a better option.
Generally speaking, it's clear as to whether or not an AngularJS $digest is already executing. But, sometimes, depending on the context, this distinction becomes blurry. Consider the following pseudo-code for a Directive link() function:
// PSEUDO-CODE for AngularJS directive link function.
function link( $scope ) {
function handler( data ) {
$scope.$apply(
function() {
// ...
}
);
}
if ( cachedData ) {
handler( cachedData );
} else {
getDataAsync( handler );
}
}
Here, we are working with data that may or may not be cached locally. If it's cached, we use it immediately; if it's not cached, we get it asynchronously. This duality causes a problem for the data handler. In one context - the cached data - the handler is called within the lifecycle of an active $digest. Then, in the other context - the asynchronous get - the handler is called outside of an AngularJS $digest.
This means that some of the time, the directive will work properly; and, some of the time, it will throw the following error:
Error: $digest already in progress
To side-step this problem, we either put in logic that explicitly checks the AngularJS $$phase (which is a big no-no!); or, we make sure that the callback handler initiates a $digest at a later time.
Up until now, my approach to deferred-$digest-invocation was to replace the $scope.$apply() call with the $timeout() service (which implicitly calls $apply() after a delay). But, yesterday, I discovered the $scope.$evalAsync() method. Both of these accomplish the same thing - they defer expression-evaluation until a later point in time. But, the $scope.$evalAsync() is likely to execute in the same tick of the JavaScript event loop.
Take a look at the following code. Notice that there are two calls to $timeout() that sandwich a call to $scope.$evalAsync():
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
$scope.$evalAsync() vs. $timeout() In AngularJS
</title>
</head>
<body>
<h1>
$scope.$evalAsync() vs. $timeout() In AngularJS
</h1>
<p bn-timing>
Check the console!
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.4.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// Test the timing of the $timeout() and $evalAsync() functions.
app.directive(
"bnTiming",
function( $timeout ) {
// I bind the JavaScript events to the local scope.
function link( $scope, element, attributes ) {
$timeout(
function() {
console.log( "$timeout 1" );
}
);
$scope.$evalAsync(
function( $scope ) {
console.log( "$evalAsync" );
}
);
$timeout(
function() {
console.log( "$timeout 2" );
}
);
}
// Return the directive configuration.
return({
link: link
});
}
);
</script>
</body>
</html>
When we run this code, we get the following console output:
$evalAsync
$timeout 1
$timeout 2
Run this demo in my JavaScript Demos project on GitHub.
Even though the first $timeout() call was before the $scope.$evalAsync() method, you can see that the $scope.$evalAsync() expression was evaluated first. This is because the $scope.$evalAsync() expressions are placed in an "async queue" that is flushed at the start of each $digest iteration. As a very high level, the $digest loop looks like this:
- Do:
-
-
-
- If asyncQueue.length, flush asyncQueue.
-
-
-
-
-
- Trigger all $watch handlers.
-
-
-
-
-
- Check for "too many" $digest iterations.
-
-
- While: ( Dirty data || asyncQueue.length )
If some aspect of the $digest phase adds an expressions to the asyncQueue (using $scope.$evalAsync()), AngularJS will perform another iteration of the $digest loop in order to flush the asyncQueue. This way, your expression is very likely to be evaluated in the same tick of the JavaScript event loop.
Of course, there are outlier cases where this isn't true, such as if the $scope.$evalAsync() puts the $digest loop over its "max iterations" limit or another expression throws an error. This is why AngularJS also uses a timeout in the $scope.$evalAsync() method. In addition to updating the asyncQueue, AngularJS also initiates a timeout that checks the asyncQueue length. This way, if the asyncQueue isn't flushed during the current $digest cycle, it will surely be flushed in a later tick of the event loop.
So, in essence, $scope.$evalAsync() combines the best of both worlds: When it can (which is most of the time), it will evaluate your expression in the same tick; otherwise, it will evaluate your expression in a later tick, which is exactly what $timeout() is doing.
I'm not saying that all instances of $timeout() should be replaced with $scope.$evalAsync() - they serve two different, albeit related, purposes. If you truly want to execute code at a later point in time, use $timeout(). However, if your only goal is tell AngularJS about a data change without throwing a "$digest already in progress" error, I would suggest using $scope.$evalAsync().
Want to use code from this post? Check out the license.
Reader Comments
Oh very interesting, thanks for the tip.
Besides, I think that using a timeout for that kind of things seems a bit like a dirty hack (kinda like the safeApply).
This seems more likely to be the correct intended usage :)
@Olivier,
Agreed. This feels like the right "intent."
@All,
***Important Node:*** The additional "timeout" that is performed in addition to the asyncQueue was not added until v1.2 of AngularJS. As such, if you try to use $evalAsync() before v1.2, you might not see the changes take place until something else explicitly performs a digest.
Thank you! This helped me today, I'm going to see of I can replace most of my timeouts with evalAsync.
This article in StackOverflow highlights a few differences of the consequences of calling evalAsync and timeout from controllers or directives:
http://stackoverflow.com/questions/17301572/angularjs-evalasync-vs-timeout
@Kelly,
Really glad to be able to help - but just take note that AngularJS 1.2 made an important change to the way $evalAsync() works (see above comment). Prior to 1.2, it didn't add the "defer" fallback. So, if you are pre-1.2, switching to $evalAsync() will cause some problems.
@Demetrius,
Dealing with the DOM and knowing when it has updated is some really interesting stuff. First off, never worry about the DOM from a Controller - the controller should not know anything about the DOM. Really, only the Directives should know about the DOM. And, if you truly want to be sure that the DOM has updated, then, yeah, using a $timeout() is probably the only fool-proof approach since the callback will be called in a later tick of the event loop.
"Watching" DOM rendering is a really complex topic. $watch() callbacks are invoked in the order in which they are bound. This gets fun! This means that, depending on your app was put together, two $watch() handlers that fire in the same digest may actually have different versions of the DOM available. Fun stuff!
@Ben,
Yup saw that note, not a problem in my case, using 1.2.4 at the moment
Ben, how controller or directive with $evalAsync can be tested? In case of usage $timeout I can use $timeout.flush() in my tests to force an execution of delayed actions.
@Jack use $scope.$digest() in the test
@Kelly,
But if function will be called asynchronously, in this case $scope.$digest doesn't fit me.
I mean that scope.$digest will trigger the digest cycle, but function passed to the evalAsync queue could be called inside $timeout func, within the next tick.
Any reason not to use `$q.when()`?
Hey Ben, i've been replacing all my calls to $timeout(fn) to scope.$evalAsync(fn) and i found that i had to revert a couple of those changes because "$digest already in progress" errors appeared. Why is that? Isn't this supposed to be a workaround for that? Changing that back to $timeout() calls, worked ok. Again, it happened for a couple of them. Any ideas?
Nice blog,
Today I solve a issue which was related to digest cycle!
$timeout was not working on few places on tizen mobile, but then i tried $scope.$evalAsync(), Its working very well with all the platforms.
Thanks.
@Alejandro,
Any idea why your $evalAsync caused some "$digest already in progress"?
Useful to know this. We use JQuery selectors
in the promise Restangular resolution code and we get this error. What is the technical explanation for this error in this type of code ?
I don't think this is the correct approach.
This is the way I handle this situation:
function link($scope) {
function handler(data) {
...
}
if ( cachedData ) {
handler(cachedData);
} else {
getDataAsync(function(data) {
handler(data);
$scope.$apply();
});
}
}
Just apply after your asynchronous events.
Thanks a lot ..
I understood the concept of avalAsync() and timeout() .
your post helped me when I created a directive to filterout links then I wanted to updated link into in Href links .
Where $timout() was perfrming action when the html was shown.
and evalAsyn() was perfrming action before the html is rendered.
for example
angular.module('myApp').directive('checkRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (attrs.taskDetails === 'true') {
scope.$evalAsync(function () {
var link = el.html();
if (link.indexOf('target="_blank"') === -1) {
var str2 = link.replace(/(https?:\/\/[^\s]+)/gi, ' a href="$1" target="_blank" $1 a');
el.html(str2);
}
});
}
}
}
});
but i still have confusion in $timeout delay value
because in code when i was performing unbind with timeout even after unbinding it was calling a function again again..
var unbind = $scope.$watch('dummy', function () {
$timeout(function () {
if ($('#tab-3').hasClass('current') == false) {
unbind();//after ubinding, it is calling syncComments again and //again
}
else {
$scope.syncComments();//ajax asynchronous requests
$scope.dummy = !$scope.dummy;
}
}, 10000);
});
Where did you get the t-shirt?
This is the ultimate self-esteem corrector. Thank you for your post; it is a very important distinction
Angulars documentation about $evalAsync states: "Executes the expression on the current scope at a later point in time."
I'm not sure what it is the actual meaning behind this, will $evalAsync run a $digest (on the current scope and its children) Or will it run $apply (on rootScope and its children) ?
If its the second, then in which cases would i use $apply?