6

There is this article I saw long time ago: https://coderwall.com/p/ngisma
It describes a method that triggers $apply if we are not in an apply or digest phase.

$scope.safeApply = function(fn) {
  var phase = this.$root.$$phase;
  if(phase == '$apply' || phase == '$digest') {
    if(fn && (typeof(fn) === 'function')) {
      fn();
    }
  } else {
    this.$apply(fn);
  }
};

Angular has the $scope.$evalAsync method (taken from 1.2.14):

   $evalAsync: function(expr) {
            // if we are outside of an $digest loop and this is the first time we are scheduling async
            // task also schedule async auto-flush
            if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
              $browser.defer(function() {
                if ($rootScope.$$asyncQueue.length) {
                  $rootScope.$digest();
                }
              });
            }

            this.$$asyncQueue.push({scope: this, expression: expr});
          }

Which calls digest if we are not in a phase and adds the current invocation to the asyncQueue.

There is also the $apply, $digest and $timeout methods. It is confusing.
What is the difference between all ways mentioned to trigger a digest cycle (a dirty check and data binding)?
What is the use case for each method?
Is safeApply() still safe? :)
What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?

No Idea For Name
  • 11,411
  • 10
  • 42
  • 70
Naor
  • 23,465
  • 48
  • 152
  • 268

2 Answers2

2

As a high level introduction I would say that it is rarely required to actually initiate your own digest cycle, since angular handles most cases.

That being said let's dive into the question.

As a very high level, the $digest loop looks like this:

Do:
- - - If asyncQueue.length, flush asyncQueue.
- - - Trigger all $watch handlers.
- - - Check for "too many" $digest iterations.
While: ( Dirty data || asyncQueue.length )

So basically $evalAsync is adding the function to the asyncQueue and defering a digest if it needs to. However if it's already in a digest cycle it will flush the asyncQueue and it will just call the function.

You may notice that this is very similar to the safeApply. One difference is that instead of adding the function to the asyncQueue it just calls it, which can happen in the middle of a cycle for instance. The other difference is that it exposes and relies on a $$ variable, which are intended to be internal.

The most important difference however between $evalAsync and $apply from $digest (I will get to $timeout below) is that $evalAsync and $apply starts the digest at the $rootScope but you call the $digest on any scope. You will need to evaluate your individual case if you think you need this flexibility.

Using $timeout is very similar to $evalAsync again except that it will always defer it out of the current digest cycle, if there is one.

$timeout(function() {
    console.log( "$timeout 1" );
});
$scope.$evalAsync(function( $scope ) {
    console.log( "$evalAsync" );
});

If you already in a digest cycle will give you

$evalAsync
$timeout 1

Even though they are called in the opposite order, since the timeout one is delayed to the next digest cycle, which it instantiates.

EDIT For the questions in the comment. The biggest difference between $apply and $evalAsync as far as I can tell is that $apply is actually triggering the $digest cycle. To you all this means is that you need to be sure you call $apply when you aren't in a digest cycle. Just for transparency, the following code is also valid:

$scope.$apply(function() {
    $scope.$evalAsync(function() {
    });
});

Which will call the apply function, add the no-op function to the $asyncQueue and begin the digest cycle. The docs say it is recommended to use $apply whenever the model changes, which could happen in your $evalAsync

The difference between fn(); $scope.apply() and $scope.apply(fn) is just that the $scope.apply(fn) does a try catch for the function and explicitly uses $exceptionHandler. Additionally, you could actually attempt to cause digest cycles in fn, which will change how the apply is handled.

I'd also like to point out at this time that it is even more complicated in 1.3 assuming it stays this way. There will be a function called $applyAsync used to defer apply calls (reference).

This post compiled some information from This blog and this so post plus some of my experience.

Hope this helped!

Community
  • 1
  • 1
hassassin
  • 5,024
  • 1
  • 29
  • 38
  • Cool! Can you explain a little bit about the differences between $apply, $evalAsync and the difference between '$apply(fn);' and 'fn(); apply();' ? – Naor Nov 06 '14 at 23:28
  • Added some stuff to the answer for this. – hassassin Nov 07 '14 at 23:11
  • Is it safe to call $evalAsyc(fn) instead of $apply(fn)? This is in order to prevent "$apply already in progress" error. Also, is it a good practice? If not, why? Thanks a lot for your answer! – Naor Nov 12 '14 at 00:21
  • I'm pretty sure it's safe. Ultimately the best practice is determining which case you actually need. It's not good practice, for instance, to always use `$evalAsync` without understanding if your code is expected to be in a digest cycle or not. – hassassin Nov 12 '14 at 17:43
1

There's only one way to start a digest cycle: Calling $digest. It's rarely called directly.

$apply is essentially a safe wrapper for $digest. Safe means that it handles errors. So most of the time this is the method you want to call to propagate changes. Another important difference is that it always starts a digest cycle at the root scope, whereas $digest can be called on any scope and only affects that scope and its descendants.

Many services and directives trigger a digest cycle, usually by calling $apply. The only important thing to know is wether you have to call $apply yourself or it has already been called by the service / directive.

Is safeApply still safe?

Yes, because there's still only one way to start a digest cycle.

What alternative we have instead of that safeApply() (in case we call $apply in the middle of a digest cycle)?

Not calling $apply in the middle of a digest cycle. Seriously. Being in that situation is almost always a design issue in the first place. It's better to resolve that issue instead of covering it.

Anyway the method used by the solution, checking $$phase, is the only way of knowing if a digest cycle is running. As shown by you it's even used by Angular internally. It's enough to do if (!$rootScope.$$phase). But keep in mind that it's a hack that might break with a new version of Angular. BTW: Monkey patching the topmost scope, as proposed by the author, would make the solution useless for isolated scopes.

Angular's way of data binding might indeed be difficult to understand for someone new to Angular at first. But basically it all boils down to this: For every change to be recognized / processed someone has to call $apply (or $digestfor that matter). That's it. This is supposed be done by directives and services. It's actually quite hard to get a '$apply already in progress' error if you have done everything right.

a better oliver
  • 26,330
  • 2
  • 58
  • 66