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:
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!
Promises have three states
.then
fulfills, and it generally analogous to a standard return value.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.
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