Scope.$applyAsync() vs. Scope.$evalAsync() in AngularJS 1.3
A while back, I looked at using the Scope.$evalAsync() method as a means to execute code asynchronously within an AngularJS context without using the $timeout() service. In AngularJS 1.3, they've added a new Scope method - Scope.$applyAsync(). After reading the documentation, the difference between new $applyAsync() method and the existing $evalAsync() method was not readily apparent. As such, I wanted to dig through the AngularJS source code to see how these two asynchronous execution methods differ.
|
|
|
||
|
|
|||
|
|
|
Both $applyAsync() and $evalAsync() will update a global queue of pending expressions (each method deals with its own global queue).
Both $applyAsync() and $evalAsync() evaluate an expression asynchronously; however, only $evalAsync() can accept a collection of override "locals" to be used during the evaluation.
Both $applyAsync() and $evalAsync() will schedule a timeout as a backup to make sure that the expression gets executed in a timely manner; however, $evalAsync() will only initialize a timer if the control flow is not already inside a $digest. $applyAsync(), on the other hand, will always schedule a timeout; that said, $applyAsync() will cancel the timer if it can evaluate the expression before the timer is needed.
Both $applyAsync() and $evalAsync() execute the pending expression inside a try/catch block and then pipe errors to the $exceptionHandler() service.
So far, the two methods appear to do exactly the same thing. The difference doesn't become evident until you look at the actual $digest execution. When AngularJS executes a digest, it walks the Scope tree and executes $watch() bindings until no more dirty data is produced. During this lifecycle, both the $applyAsync() queue and the $evalAsync() queue get flushed; but, this happens in two very different places.
The $applyAsync() queue only gets flushed at the top of the $digest before AngularJS starts checking for dirty data. As such, the $applyAsync() queue will be flushed, at most, one time during a $digest and will only get flushed if the queue was already populated before the $digest started.
NOTE: Within the $digest, $applyAsync() will only flush if the current scope is the $rootScope. This means that if you call $digest on a child scope, it will not implicitly flush the $applyAsync() queue.
The $evalAsync() queue, on the other hand, is flushed at the top of the while-loop that implements the "dirty check" inside the $digest. This means that any expression added to the $evalAsync() queue during a digest will be executed at a later point within the same digest.
To make this difference more concrete, it means that asynchronous expressions added by $evalAsync() from within a $watch() binding will execute in the same digest. Asynchronous expressions added by $applyAsync() from within a $watch() binding will execute at a later point in time (~10ms).
Outside of a $digest, both the $applyAsync() method and the $evalAsync() method will populate their respective queues and then initialize a timer. When the timer executes, it triggers a $digest on the $rootScope.
Now, even with this level of understanding, it's still not entirely clear what the need for the new $applyAsync() method is. I suppose it's somewhat more likely to allow for DOM (Document Object Model) rendering before the expression is evaluated; but, I don't believe this is guaranteed (depending on when $watch() bindings were initialized).
To get more answers, I looked in the AngularJS source code to see if the $applyAsync() method was being used internally. And it was - within the $http service. It looks like the $httpProvider now allows you to flag the use of $applyAsync() so that AJAX (Asynchronous JavaScript and JSON) requests that complete within a ~10ms timeframe can be grouped into the same $digest (rather than triggering an individual $digest per AJAX response).
So, this looks like a micro-optimization that allows you to group sets of asynchronous expressions. I'll have to noodle on this a bit more; even after all this, I'm still not sure I would know when or why to replace $evalAsync() method calls with $applyAsync() method calls.
Reader Comments
I found it useful to rename $evalAsync. I call it $digestAsync
Now, if we compare $digestAsync vs. $applyAsync it is quite obvious what are the differences:
1. $applyAsync queues a request for $apply while $digestAsync queues a request for $digest
2. Executing $applyAsync inside apply phase will not cause additional apply while executing $digestAsync inside of a digest cycle will not cause additional digest cycle
Which one to use ?
I guess the answer is the same as asking "should I use $apply or $digest?"
@Ori,
I think you are not completely correct, they both execute $rootScope.$digest()
May be I did not get the idea of you message
Thank you for the nice article!
I liked it and decided to translate into Russian on my blog - http://stepansuvorov.com/blog/2015/05/%D0%BE%D1%82%D0%BB%D0%B8%D1%87%D0%B8%D0%B5-applyasync-%D0%BE%D1%82-evalasync-%D0%B2-angular-1-3/
Nice post, can you tell me how you get the nice Angular outputs in your console, is this some add-on or you did it yourself?
Another key service that uses $evalAsync is $q, I've been tracing through its functionality.
Can you tell me anything whether there is any difference between how the DOM gets repainted when using $apply / $applyAsync / $evalAsync?
I'm trying to help out on an indexedDB library and we're finding that making a series of async calls which invoke $rootScope.$apply is repainting the DOM on each call. I've found that since $q.defer().resolve(...) implicitly invokes $evalAsync and therefore $digest, I'm wondering if we should avoid directly calling $apply and just rely on letting $q take care of handling the digest for us... Any thoughts?
https://github.com/bramski/angular-indexedDB/issues/19
Here's some information on how DOM repaints are impacted.
https://docs.angularjs.org/guide/scope#integration-with-the-browser-event-loop
@Danny,
All good questions. All of the DOM manipulation, in AngularJS, is done through directives like ngIf, ngRepeat, ngShow, ngHide, etc. Each of these handles DOM manipulation for their particular use-case; so, there's not one-size-fits-all explanation. That said, the directives do seem to follow the general implementation pattern:
function link( scope, element ) {
. . . scope.$watch( someAttribute, function() { ... do DOM manipulation ... } );
}
Typically, the directive does DOM manipulation in response to a $watch() callback. This is why it is generally *safer* to query the DOM in $evalAsync() if that DOM is being manipulated by another directive -- it gives that other directive "time" to mutate things during its $watch() callbacks.
That said, AngularJS is also very tightly integrated with asynchronous things like $timeout() and $q(). If you are using those, you do NOT NEED to explicitly call $apply() since those services will automatically call it for you. As such, if you call it directly, you're only making the framework do more work.
Without more details, that's really all I can say.
@Marco,
The output is just from the "console" object in Firefox's Firebug plugin. But, Chrome devtools has very similar features:
console.log( "some data" )
console.info( "some data" );
console.error( "an errror" );
console.warn( "a warning" );
console.table( "an array of hashes" );
There's even more functions than that - I think the console object can do "timing" and what not, but I've never really used it.
@Stepan,
Super cool! Thanks so much for the love :D
@Ori,
I would probably just default to using $evalAsync() unless I had a reason to using $applyAsync(). Since I've written this, I don't think that I've actually come across a use case for it that really made a lot of sense in my head. But, I'll keep thinking.
@Ben,
Thanks for your reply. I was more keen on understanding rendering, rather than manipulation; as in, how repaints and re-renders get triggered after the $digest process and whether there is a difference in how things get rendered if using $apply versus $applyAsync versus $evalAsync. It sounded from the article like $applyAsync has about a 10ms threshold for subsequent async requests to get 'grouped' together in the same $digest: "requests that complete within a ~10ms timeframe can be grouped into the same $digest (rather than triggering an individual $digest per AJAX response)." So, I was curious whether you knew if $evalAsync worked similarly and whether this means that angular will only trigger 1 DOM re-render for these grouped updates. Sounds like $apply would trigger a re-render every time it is called.
I'm currently investigating some performance issues on a angular indexedDB service here: https://github.com/bramski/angular-indexedDB/issues/19
We've noticed that the UI freezes when we use an upsert method over a large data set (the example was upserting 1000+ items into an indexed DB). Mr. Spock would say that the UI freezing when making a database transaction is highly illogical. I'm not sure how familiar you are with indexedDB, but it was written to work through async transactions; so when upserting an item into the database, in angularland you would ideally use the $q deferred/promise approach and handle the results of the transaction in a .then().catch() chain. Or with the built in .onsuccess .onerror listeners that come stock in vanilla JS. The problem is that the original author of the library created an arbitrary wrapper around $q, then wrapped every single one of these database transaction methods with $rootScope.$apply and used his strange $q wrapper concoction to resolve the request. We believe the use of $apply is a dealbreaker because it's triggering a re-render for each of the upserts that get applied to the database. Not to mention pointless since $q handles the apply for us already.
(phew)
Anyway, that's the situation. After doing some digging, we found that since $q contains a call to $evalAsync internally, calling $apply explicitly is (exactly like you said) causing the framework to do MUCH more work. I guess one of the takeaways for people is to wield $apply, $applyAsync, and $evalAsync carefully, especially when using the $http or $q services, since both of those do that internally already. But that's broaching a completely different topic which I'm sure you've already covered :]
Thanks again!
(and sorry about the novel)
@Stepan,
no , only $applyAsync() execute $rootscope.$digest(). $digestAsync() ,though there is no such thing , runs digest cycle for its current scope and its children scopes , which is not similar to $rootscope.$digest().
I ran into interesting situation:
1. $evalAsync in primary component adds new item to the queue.
2. It is however cleared in the $digest run by another component on its scope (imagine both catch document.addEventListener('click', ...).
3. So the scope of the primary component is not updated (cf. if (asyncQueue.length) condition in $evalAsync).
So effectively another component stole $digest command to my component which remains unupdated.
1. executes evalasync of expression in current scope while current scope digest is in progress( scope.$digest() is in progress not $rootScope.digest() )
2. So applyasync queue will not be flushed
3. but at every digest loop evalasync queue is going to be flushed, that means there is no $rootScope.$digest() happens at this time.
4. will it contradict with the statement
"Outside of a $digest, both the $applyAsync() method and the $evalAsync() method will populate their respective queues and then initialize a timer. When the timer executes, it triggers a $digest on the $rootScope."
5. that means sometime evalasync triggers $rootScope.$digest() by triggering timeout and sometime not triggers $rootScope.$digest() (as evalasync is already executed in the digest of individual scope(scope$digest() ) while it is in progress)