Sometimes I need to use $scope.$apply
in my code and sometimes it throws a "digest already in progress" error. So I started to find a way around this and found this question: AngularJS : Prevent error $digest already in progress when calling $scope.$apply(). However in the comments (and on the angular wiki) you can read:
Don't do if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.
So now i have two questions:
Why exactly is this an anti-pattern? How can i safely use $scope.$apply?
Another "solution" to prevent "digest already in progress" error seems to be using $timeout:
$timeout(function() {
//...
});
Is that the way to go? Is it safer? So here is the real question: How I can entirely eliminate the possibility of a "digest already in progress" error?
PS: I am only using $scope.$apply in non-angularjs callbacks that are not synchronous. (as far as I know those are situations where you must use $scope.$apply if you want your changes to be applied)
scope
from within angular or from outside of angular. So according to this you always know, if you need to call scope.$apply
or not. And if you are using the same code for both angular/non-angular scope
manipulation, you're doing it wrong, it should be always separated... so basically if you run into a case where you need to check scope.$$phase
, your code is not designed in a correct way, and there is always a way to do it 'the right way'
digest already in progress
error
After some more digging i was able to solve the question whether it's always safe to use $scope.$apply
. The short answer is yes.
Long answer:
Due to how your browser executes Javascript, it is not possible that two digest calls collide by chance.
The JavaScript code we write doesn’t all run in one go, instead it executes in turns. Each of these turns runs uninterupted from start to finish, and when a turn is running, nothing else happens in our browser. (from http://jimhoskins.com/2012/12/17/angularjs-and-apply.html)
Hence the error "digest already in progress" can only occur in one situation: When an $apply is issued inside another $apply, e.g.:
$scope.apply(function() {
// some code...
$scope.apply(function() { ... });
});
This situation can not arise if we use $scope.apply in a pure non-angularjs callback, like for example the callback of setTimeout
. So the following code is 100% bulletproof and there is no need to do a if (!$scope.$$phase) $scope.$apply()
setTimeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 2000);
even this one is safe:
$scope.$apply(function () {
setTimeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 2000);
});
What is NOT safe (because $timeout - like all angularjs helpers - already calls $scope.$apply
for you):
$timeout(function () {
$scope.$apply(function () {
$scope.message = "Timeout called!";
});
}, 2000);
This also explains why the usage of if (!$scope.$$phase) $scope.$apply()
is an anti-pattern. You simply don't need it if you use $scope.$apply
in the correct way: In a pure js callback like setTimeout
for example.
Read http://jimhoskins.com/2012/12/17/angularjs-and-apply.html for the more detailed explanation.
It is most definitely an anti-pattern now. I've seen a digest blow up even if you check for the $$phase. You're just not supposed to access the internal API denoted by $$
prefixes.
You should use
$scope.$evalAsync();
as this is the preferred method in Angular ^1.4 and is specifically exposed as an API for the application layer.
In any case when your digest in progress and you push another service to digest, it simply gives an error i.e. digest already in progress. so to cure this you have two option. you can check for anyother digest in progress like polling.
First one
if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
$scope.$apply();
}
if the above condition is true, then you can apply your $scope.$apply otherwies not and
second solution is use $timeout
$timeout(function() {
//...
})
it will not let the other digest to start untill $timeout complete it's execution.
$scope.$apply();
.
$timeout
is the key! it works and later i found that it is recommended too.
scope.$apply
triggers a $digest
cycle which is fundamental to 2-way data binding
A $digest
cycle checks for objects i.e. models(to be precise $watch
) attached to $scope
to assess if their values have changed and if it detects a change then it takes necessary steps to update the view.
Now when you use $scope.$apply
you face an error "Already in progress" so it is quite obvious that a $digest is running but what triggered it?
ans--> every $http
calls, all ng-click, repeat, show, hide etc trigger a $digest
cycle AND THE WORST PART IT RUNS OF EVERY $SCOPE.
ie say your page has 4 controllers or directives A,B,C,D
If you have 4 $scope
properties in each of them then you have a total of 16 $scope properties on your page.
If you trigger $scope.$apply
in controller D then a $digest
cycle will check for all 16 values!!! plus all the $rootScope properties.
Answer-->but $scope.$digest
triggers a $digest
on child and same scope so it will check only 4 properties. So if you are sure that changes in D will not affect A, B, C then use $scope.$diges
t not $scope.$apply
.
So a mere ng-click or ng-show/hide might be triggering a $digest
cycle on over 100+ properties even when the user has not fired any event!
Use $timeout
, it is the way recommended.
My scenario is that I need to change items on the page based on the data I received from a WebSocket. And since it is outside of Angular, without the $timeout, the only model will be changed but not the view. Because Angular doesn't know that piece of data has been changed. $timeout
is basically telling Angular to make the change in the next round of $digest.
I tried the following as well and it works. The difference to me is that $timeout is clearer.
setTimeout(function(){
$scope.$apply(function(){
// changes
});
},0)
$http
). Otherwise you have to repeat this code all over the place.
$scope.$apply
if you are using setTimeout
or $timeout
I found very cool solution:
.factory('safeApply', [function($rootScope) {
return function($scope, fn) {
var phase = $scope.$root.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (fn) {
$scope.$eval(fn);
}
} else {
if (fn) {
$scope.$apply(fn);
} else {
$scope.$apply();
}
}
}
}])
inject that where you need:
.controller('MyCtrl', ['$scope', 'safeApply',
function($scope, safeApply) {
safeApply($scope); // no function passed in
safeApply($scope, function() { // passing a function in
});
}
])
Success story sharing
$document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });
I really don't know why I have to make $apply here, because I'm using $document.bind..function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }
There is no apply in there.$timeout
semantically means running code after a delay. It might be a functionally safe thing to do but it is a hack. There should be a safe way to use $apply when you're unable to know whether a$digest
cycle is in progress or you're already inside an$apply
.