Angular $scope.$apply vs $timeout as a safe $apply
I'm trying to better understand the nuances of using the $timeout service in Angular as a sort of "safe $apply" method. Basically in scenarios where a piece of code could run in response to either an Angular event or a non-angular event such as jQuery or some standard DOM event.
As I understand things:
- Wrapping code in $scope.$apply works fine for scenarios where you aren't already in a digest loop (aka. jQuery event) but will raise an error if a digest is in progress
- Wrapping code in a $timeout() call with no delay parameter works whether already in a digest cycle or not
Looking at Angular source code, it looks like $timeout makes a call to $rootScope.$apply().
- Why doesn't $timeout() also raise an error if a digest cycle is already in progress?
- Is the best practice to use $scope.$apply() when you know for sure that a digest won't already be in progress and $timeout() when needing it to be safe either way?
- Is $timeout() really an acceptable "safe apply", or are there gotchas?
Thanks for any insight.
Solution 1:
Looking at Angular source code, it looks like $timeout makes a call to $rootScope.$apply().
- Why doesn't $timeout() also raise an error if a digest cycle is already in progress?
$timeout
makes use of an undocumented Angular service $browser
. Specifically it uses $browser.defer()
that defers execution of your function asynchronously via window.setTimeout(fn, delay)
, which will always run outside of Angular life-cycle. Only once window.setTimeout
has fired your function will $timeout
call $rootScope.$apply()
.
- Is the best practice to use $scope.$apply() when you know for sure that a digest won't already be in progress and $timeout() when needing it to be safe either way?
I would say so. Another use case is that sometimes you need to access a $scope variable that you know will only be initialized after digest. Simple example would be if you want to set a form's state to dirty inside your controller constructor (for whatever reason). Without $timeout the FormController
has not been initialized and published onto $scope, so wrapping $scope.yourform.setDirty()
inside $timeout ensures that FormController
has been initialized. Sure you can do all this with a directive without $timeout, just giving another use case example.
- Is $timeout() really an acceptable "safe apply", or are there gotchas?
It should always be safe, but your go to method should always aim for $apply() in my opinion. The current Angular app I'm working on is fairly large and we've only had to rely on $timeout once instead of $apply().
Solution 2:
If we use $apply heavily in the application, we might get the Error: $digest already in progress. It happens because one $digest cycle can be run at a time. We can resolve it by $timeout or by $evalAsync.
The $timeout does not generate error like "$digest already in progress“ because $timeout tells Angular that after the current cycle, there is a timeout waiting and this way it ensures that there will not any collisions between digest cycles and thus output of $timeout will execute on a new $digest cycle.
I tried to explain them at : Comparison of apply, timeout,digest and evalAsync.
May be it will help you.
Solution 3:
As far as I understand it, $timeout
is a wrapper around setTimeout
which implicitly calls $scope.$apply
, meaning it runs outside of the angular lifecycle, but kickstarts the angular lifecycle itself. The only "gotcha" I can think of is that if you're expecting your result to be available this $digest
, you need to find another way to "safe apply" (which, AFAIK, is only available via $scope.$$phase
).