The Power of Angular $q
Dave Smith
@djsmith42
http://thesmithfam.org/blog
No Bad Jokes
Typically I begin my presentations with a bad joke, but today, I promise I will not do that.
deferred.resolve(punchline);
Passing callbacks as function arguments
function getCurrentUser(callback) { $http.get("/api/users/me") .success(function(user) { callback(user); }); }
The Old Way
Calling It
getCurrentUser(function(user) { // Do something with user });
Adding Another Request
function getPermissions(callback) { $http.get("/api/permissions") .success(function(permissions) { callback(permissions); }); }
Calling Both...
getCurrentUser(function(user) { // Do something with user }); getPermissions(function(permissions) { // Do something with permissions }); // Do something with user and permissions... // How???
Serial?
getCurrentUser(function(user) { getPermissions(function(permissions) { // Do something with permissions
// Do something with user
});
});
Does not scale
getCurrentUser(function(user) { getPermissions(function(permissions) {
getFoo(function(foo) {
getBar(function(bar) {
// :(
});
});
});
});
Problems:
- Not parallelizable
- Not composable
- Not dynamic
With $q, we can do better!
But how do I use $q?
Two simple steps:
- Stop passing callbacks as parameters
- Start returning promises
The $q Way
function getCurrentUser() { var deferred = $q.defer(); $http.get("/api/users/me") .success(function(user) { deferred.resolve(user); }); return deferred.promise; }
Calling It
getCurrentUser().then(function(user) { // Do something with user });
Looks similar, but it's very different.
Why is this better?
Suddenly, all your async operations become:
- Parallelizable
- Composable
- Dynamic
Parallelizable
$q.all([getCurrentUser(), getPermissions()]) .then(function(responses) { var user = responses[0]; var permissions = responses[1]; });
By the way, this can be even nicer...
My favorite spread
My second favorite spread
function spread(func) { return function(array) { return func.apply(void 0, array); } }
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void
https://github.com/kriskowal/q/blob/v1/q.js
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
How to use spread:
$q.all([getCurrentUser(), getPermissions()]) .then(spread(function(user, permissions) { // Do something with user and permissions }));
Almost as delicious as
Composable
var promise1 = foo(); var promise2 = promise1.then(function() {...}); var promise3 = promise2.then(function() {...}); // By the way: expect(promise1 === promise2).toBe(false);
expect(promise2 === promise3).toBe(false);
Promises are (mostly) immutable
Dynamic
var promises = [getCurrentUser()]; if (shouldGetPermissions) { promises.push(getPermissions()); } $q.all(promises).then(function() { // all done! });
"runtime logic" vs. "static logic"
What else?
Why this API separation?
deferred.resolve() promise.then()
This forces a separation between notifier and receiver
What about errors?
deferred.reject()
promise.then(function() { // success }, function() { // failure
});
The notifier can send errors like this:
The receiver can receive them like this:
What about status?
deferred.notify(...)
promise.then(function() { // success }, function() { // failure }, function() { // progress (called 0 or more times) });
The notifier can send progress updates like this:
The receiver can receive them like this:
Deferreds: One Time Use
Once you resolve or reject a deferred object, it cannot be resolved or deferred again.
But you can be late to the party
You can connect a .then() to a promise after its deferred has been resolved, and your callback will be called at the next digest loop.
Last but not least
$q.when()
$q.when()
"Everything's better wrapped in a promise..."
"...even promises"
Easy Client-Side Caching
myApp.factory("movies", function($q, $http) {
var cachedMovies;
return {
getMovies: function() {
return $q.when(cachedMovies || helper());
}
};
function helper() {
var deferred = $q.defer();
$http.get("/movies").success(function(movies) {
cachedMovies = movies;
deferred.resolve(movies);
});
return deferred.promise;
}
});
Hardened Client-Side Caching
myApp.factory("movies", function($q, $http) {
var cachedMovies, p;
return {
getMovies: function() {
return $q.when(cachedMovies || p || helper());
}
};
function helper() {
var deferred = $q.defer();
p = deferred.promise;
$http.get("/movies").success(function(movies) {
cachedMovies = movies;
deferred.resolve(movies);
});
return deferred.promise;
}
});
Side Note
$http already does caching.
Look at the "cache" argument.
https://docs.angularjs.org/api/ng/service/$http
Promises wrapped in
promises wrapped in
promise wrapped in
...
Promises are Composable
$q is Angular's Champagne
- All $http requests use $q
- Request interceptors
- Response interceptors
- $interval and $timeout
- ngAnimate
- "ngAnimatias"
- ngModelController (async validators)
- $templateRequest
Why Did Angular Write $q?
- Why not just the original "Q"?
- $q is aware of the digest loop.
- The Angular team is freaking brilliant.
it('should simulate promise', inject(function($q, $rootScope) {
var deferred = $q.defer();
var promise = deferred.promise;
var resolvedValue;
promise.then(function(value) {
resolvedValue = value;
});
deferred.resolve(123);
expect(resolvedValue).toBeUndefined();
$rootScope.$apply();
expect(resolvedValue).toEqual(123);
}));
Best Explained via Test
<-- What?
<-- Oh! Ok.
Promises are Everywhere
- Even jQuery has them ;-)
- Q
- Bluebird
- Native promises coming soon
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://promisesaplus.com/implementations
In Conclusion
function doSomething(callback) { ... callback(); } function doSomething() { ... return promise; }
The Power of Angular $q
By djsmith
The Power of Angular $q
- 9,478