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

后端 未结 2 834
南旧
南旧 2020-11-27 19:06

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 mu

2条回答
  •  旧巷少年郎
    2020-11-27 19:51

    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

提交回复
热议问题