Manually calling $digest on a scope prevents $rootScope.$digest from executing for pending $evalAsyncs #15127
Description
Issue
This is a bug report for an intermittent issue we are seeing where due to specific timing of asynchronous callbacks executing causes $rootScope.$digest from being called when it is expected that Angular should otherwise do so.
I have a plunkr that simulates the scenario to induce the failure every time.
The current behavior is that a controller "A" scope is not digested upon conclusion of a $.then when another controller "B" scope's $digest is called manually.
This plunkr demonstrates the issue. Please follow these steps:
- Click "Test (Bugged)"
- Observe that ctrl.result changes to "Started" but does not end with "Finished". Nor does ctrl.shared.foo display the same value as ctrl2.shared.foo.
The expected behavior is that ctrl.result ends displaying "Finished" and ctrl.shared.foo matches what is shown for ctrl2.shared.foo.
What is being demonstrated is the $digest for ctrl2 causes the digest for ctrl to be skipped. Because ctrl is using $q the expectation is that its scope will be digested at the conclusion of the $q.then function (presumably as a result of a $rootScope.$digest being performed by Angular).
- After clicking "Reset", click the "Test (Workaround)" to see the test using a hack we are currently using in some of our code to workaround the issue. Note how the values displayed for ctrl are as expected.
This workaround (undesirably) inspects$scope.$ $asyncQueue.length and if there are any values, it avoids the manual call to $scope.$digest. This permits Angular's deferred call to process the queue and rootScope.digest to later run, and everything is digested as expected.
It appears that all browsers and all "recent" versions of Angular exhibit the issue.
What is going on?
In our application, here is what's going on:
- Controller A and B are servicing completely separate parts of the UI/DOM. Their scopes are in different branches.
- Controller A performs an asynchronous operation (outside of angular) which it wraps in a $q.when. When the data becomes available, the corresponding $q.then is called where we load up the controller's model and then we expect Angular to digest the changes and update the DOM.
- Controller B likewise is performing an asynchronous operation (via a service actually, but that's irrelevant to honing in on the issue). However, it is not using $q and instead performs a manual call to $scope.$digest() on its own scope branch due to performance reasons. The motivation for this is an optimization to prevent digesting the entire app for something that is happening extremely frequently and affects just a tiny portion of the UI.
- Normally these two completely unrelated things work just fine, and we get what we are after with regard to Controller B's performance impact.
- However, on some occasions typically when the browser gets busy for a bit, Controller B's $digest stomps on Controller A's digest.
Tracing through Angular here is what I've come up with:
- Controller A's async result comes in and corresponding callback invocation (which involves $q.then) is in the browser's dispatch queue. At some point just before this callback being invoked Controller B's async result callback is pushed to the queue. This is the key to seeing the failure -- you need both callbacks to be in the browser's queue.
- Controller A's callback is then called as expected. Angular adds the .then call to the $$asyncQueue and defers a call to flush the $$asyncQueue and do a $rootScope.$digest.
- Controller B's callback is then called. Controller B's scope.$digest is called. It sees that the $$asyncQueue has stuff to do, so it does it -- without a $rootScope.$digest.
- The deferred call from step 2 executes, sees that asyncQueue is empty, and so it doesn't do anything.
It seems like scope.$digest shouldn't be processing the asyncQueue like this unless the scope being digested is the rootScope.