AngularJS : $q -> deferred API order of things (lifecycle) AND who invokes digest?
Asked Answered
F

2

9

The $q service is very powerful in angularjs and make our life easier with asynchronous code.

I am new to angular but using deferred API is not very new to me. I must say that I completely ok with the How to use part of documentation + there are very useful links for that within the docs + I checked out the source either.

My question is more about the under the hood parts of deferred and promise API objects in angular. What are the exact phases in their life cycles and how are they interacts with rootScope.Scope(s). My assumptions are that when the promise resolves - it invokes the digest loop ??? yes / no ?

Can one provide a detailed answer with specific respect to the following list of aspects:

  1. What is the order of things that happen in each of your described steps / phases
  2. When new deferred object with a new promise instance created - who aware of it / is it important ?
  3. How exactly the scope updated when promise object being resolved? Do i have to update it manually inside the callback or the digest will be automatically invoked and update the rootScope like declared here
  4. mention at least one approach of updating the scope from within the promise callback
  5. I assume there a lot of other useful aspects, feel free to provide them all.

I will appreciate and accept the most detailed answer, with as much as possible references to docs or source (that i couldn't find by myself). I can't find any previously discussion with this topic, if there already was - please post links.

ps: +1 for any one that will help by suggesting a better title for this question, please add your suggestions in a comment.

Cheers!

Flyaway answered 29/4, 2014 at 11:11 Comment(0)
O
25

Promises have three states

  • Pending - this is how promises start.
  • Fulfilled - this is what happens when you resolve a deferred, or when the return value from .then fulfills, and it generally analogous to a standard return value.
  • Rejected - This is what happens when you reject a deferred, when you throw from a .then handler or when you return a promise that unwraps to a rejection*, it is generally analogous to a standard exception thrown.

In Angular, promises resolve asynchronously and provide their guarantees by resolving via $rootScope.$evalAsync(callback); (taken from here).

Since it is run via $evalAsync we know that at least one digest cycle will happen after the promise resolves (normally), since it will schedule a new digest if one is not in progress.

This is also why for example when you want to unit test promise code in Angular, you need to run a digest loop (generally, on rootScope via $rootScope.digest()) since $evalAsync execution is part of the digest loop.

Ok, enough talk, show me the code:

Note: This shows the code paths from Angular 1.2, the code paths in Angular 1.x are all similar but in 1.3+ $q has been refactored to use prototypical inheritance so this answer is not accurate in code (but is in spirit) for those versions.

1) When $q is created it does this:

  this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
    return qFactory(function(callback) {
      $rootScope.$evalAsync(callback);
    }, $exceptionHandler);
  }];

Which in turn, does:

function qFactory(nextTick, exceptionHandler) {

And only resolves on nextTick passed as $evalAsync inside resolve and notify:

  resolve: function(val) {
    if (pending) {
      var callbacks = pending;
      pending = undefined;
      value = ref(val);

      if (callbacks.length) {
        nextTick(function() {
          var callback;
          for (var i = 0, ii = callbacks.length; i < ii; i++) {
            callback = callbacks[i];
            value.then(callback[0], callback[1], callback[2]);
          }
        });
      }
    }
  },

On the root scope, $evalAsync is defined as:

  $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});
  },

  $$postDigest : function(fn) {
    this.$$postDigestQueue.push(fn);
  },

Which, as you can see indeed schedules a digest if we are not in one and no digest has previously been scheduled. Then it pushes the function to the $$asyncQueue.

In turn inside $digest (during a cycle, and before testing the watchers):

 asyncQueue = this.$$asyncQueue,
 ...
 while(asyncQueue.length) {
      try {
          asyncTask = asyncQueue.shift();
          asyncTask.scope.$eval(asyncTask.expression);
      } catch (e) {
          clearPhase();
          $exceptionHandler(e);
      }
      lastDirtyWatch = null;
 }

So, as we can see, it runs on the $$asyncQueue until it's empty, executing the code in your promise.

So, as we can see, updating the scope is simply assigning to it, a digest will run if it's not already running, and if it is, the code inside the promise, run on $evalAsync is called before the watchers are run. So a simple:

myPromise().then(function(result){
    $scope.someName = result;
});

Suffices, keeping it simple.

* note angular distinguishes throws from rejections - throws are logged by default and rejections have to be logged explicitly

Osteoporosis answered 29/4, 2014 at 11:23 Comment(3)
Thanks! And once again for revealing the relevant source parts! I will definitely accept this answer, completely perfect! I feel that i can safely move on now )))Flyaway
@Flyaway sure, enjoy promises - they're truly a useful abstraction. Also, it would be great if now that you understand how it works, you could support this proposal :) github.com/angular/angular.js/issues/6697Osteoporosis
This answer explained a lot. We were using deferreds in our tests and it took quite a while for us to realise that we had to digest once until it was resolved.Overman
U
8

when the promise resolves - it invokes the digest loop ?

Yes. You can test this by a simple template:

{{state}}

and controller code that changes the $scope.state variable after a delay:

$scope.state = 'Pending';
var d = $q.defer();
d.promise.then(function() {
  $scope.state = 'Resolved, and digest cycle must have run';
});
$window.setTimeout(function() {
  d.resolve();
}, 1000);

You can see this at http://plnkr.co/edit/fIfHYz9EYK14A5OS6NLd?p=preview. After a second, the text in the HTML shows Resolved, and digest cycle must have run. The call to setTimeout rather than $timeout is deliberate, to ensure that it must be resolve which ends up starting the digest loop.

This is confirmed by looking in the source: resolve calls its callbacks via nextTick, which is a function that passed the callbacks to $rootScope.$evalAsync, which according to the $evalAsync docs:

at least one $digest cycle will be performed after expression execution

Those same docs also say:

Note: if this function is called outside of a $digest cycle, a new $digest cycle will be scheduled

so whether the stack is already in the $digest loop can change the exact order of events.


'1. What is the order of things that happen in each of your described steps / phases

Going into the previous example in detail:

  1. var d = $q.defer(); The deferred object's promise is in a pending state. At this point, virtually nothing has happened, you just have a deferred object with resolve, reject, notifiy and promise properties. Nothing has used or affected the $digest loop

  2. d.promise.then(function() { $scope.state = 'Resolved, and digest cycle must have run'; });

    The promise is still in a pending state, but with a then success callback registered. Again, nothing has used or affected the $digest loop or any scope.

  3. $window.setTimeout(function() { d.resolve(); }, 1000);

    After 1 second, d.resolve will be called. This passes the callback defined in Step 2 above to $evalAsync (via nextTick).

  4. $evalAsync will call the callback

  5. $evalAsync will then ensure one $digest cycle will be called.


'2. When new deferred object with a new promise instance created - who aware of it / is it important ?

Only the caller of $q.defer(). Nothing happens with respect to any scope until resolved is called (or indeed, reject or notify).


'3. How exactly the scope updated promise object being resolved? Do i have to update it manually inside the callback or the digest will be automatically invoked and update the rootScope like declared here

As mentioned, the $digest loop will be automatically started by calling resolve (if it's not in it already).


'4. mention at least one approach of updating the scope from within the promise callback

The example above gives this.


'5. I assume there a lot of other useful aspects, feel free to provide them all.

Not that I can think of!

Uzial answered 29/4, 2014 at 13:0 Comment(2)
I'm not really sure why you added another answer an hour later. Would you mind explaining what you think this answer explains that mine does not so I can better understand what you think mine is missing? Sorry, and thanks.Osteoporosis
@BenjaminGruenbaum I don't think my answer particularly explains anything yours doesn't. I wanted to given an answer that tried to be clearer and focused on different things, by a) Using a short example rather than the source of Angular, with a working Plunker to see it working; b) addressing the OPs questions individually, especially regarding the order of what happens when a promise is created -> resolved; c) omitting the introducing about states of promises: the OP suggests this isn't required.Uzial

© 2022 - 2024 — McMap. All rights reserved.