Tuesday, September 8, 2015

Getting Inside Angular: $scope.$evalAsync

If you want something to be executed while a digest is in progress, then $scope.$evalAsync is probably what you're looking for.

What does $scope.$evalAsync do?

When a digest is in progress, Angular is looping through all the watches and comparing values to detect if any changes have occurred. As the listener function from a watch could cause a change to another watched property, this loop can run multiple times per digest. $scope.$evalAsync adds the supplied function to a queue that will be drained at the beginning of the next loop in a digest cycle. It won't wait for the current digest cycle to complete but will execute the $evalAsync'ed function before starting the next loop through all the watches. If a digest is not in progress, one will be scheduled when a function is added to the async queue.

When would you want to use it?

A good explanation of when you would want to use $scope.$evalAsync can be found here: http://stackoverflow.com/questions/17301572/angularjs-evalasync-vs-timeout

In summary, as it queues work to occur outside of the current stack frame:
  • In a Controller: it will run before the DOM has been manipulated by Angular (take caution as your change could be overwritten) and therefore, before the browser renders
  • In a Service: it will run after the DOM has been manipulated by Angular but before the browser renders
If your change is going to affect something that is being rendered, $evalAsync will prevent a flicker from occurring as its work will occur before rendering.

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
  
  $scope.$watch(
    function (scope) { return scope.name; },
    function (newValue, oldValue, scope) {
      
      console.log('First watch executed \nAdding $evalAsync');
      scope.$evalAsync(function (scope) {
        
        console.log('$evalAsync executed');
        scope.newValue = "new value!";
      });
    }
  );
  
  $scope.$$postDigest(function () {
    
    console.log('$$postDigest executed. Digest completed');
    console.log($scope.newValue);
  });
});

View on Plunker (open the developer tool's console).

The above example ensures that $evalAsync is being called (line 9) during a digest by calling it in the listener function of a watch. The work that is being added to the async queue logs a message out to the console to indicate that the work has been executed and then updates a value on the scope. Line 17 adds a function to the post digest queue* to indicate when the digest has completed.

From the output it is clear to see that the function that was added to the async queue was in fact executed during the already in progress digest.

*The post digest queue is drained once all the change detection loops of the watches have completed and is the last step of a digest that you can interact with. However, the Angular team would prefer that you don't use it--or that you take great care if you do--by marking the $$postDigest function private by prefixing it with two dollar signs.

Performance considerations

Adding to the async queue is either going to cause a new digest to occur or if one is in progress, for it to loop one more time through the watches. If the queued work causes changes to other properties that are under watch, this will lead to additional loops. Angular has a mechanism to prevent a digest from continuing forever and will only allow 10 loops to occur before the digest is terminated (known as TTL: Time To Live). As you cannot control at what point during a digest (in other words, how many loops it has already performed) your work is being added, it could be approaching its TTL limit. This is something to consider when using $evalAsync.

Versus $scope.$applyAsync

Both $applyAsync and $evalAsync are tools that help you get around "$digest already in progress" errors. In my opinion, $applyAsync is the safer option as this queue will be flushed before a new digest begins. This will reduce the likelihood that your work will cause a digest to be terminated due to reaching the TTL limit. There is the potential though that using $applyAsync will cause a flicker as you will be relinquishing control back to the browser before your queued work is executed. If the browser decides to render first, then a flicker could occur. Some might prefer $evalAsync for this reason as it won't leave them at the mercy of the browser's event loop.

//Code, rinse, repeat