$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 );
}
}
view rawpseudo-code.js hosted with ❤ by GitHub

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>
view raweval-async.htm hosted with ❤ by GitHub

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().

FROM HERE

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s