Continuing from the previous post; lets dive into ways of using promises. First, some helper functions:
var log = console.log.bind(console); function delay(fn, time){ setTimeout(function() { fn(time); }, time); }
1. Pseudo-Observers
This involves attaching several handlers to a single promise; all attached handlers are invoked once the promise resolves. I call this the ‘pseudo-observer’ pattern because each ‘observer’ gets invoked once (remember promises never leave their resolution state). This explains why promises can’t be used to implement pubsub.
p = new Promise(function(resolve,reject){ delay(resolve, 2000); }); function foo(){ p.then(function(val) { log("foo called after " + val + "ms"); }); } function bar(){ p.then(function(val) { log("bar called after " + val + "ms"); }); } foo(); bar(); // -- After 2 seconds -- //log: foo called after 2000ms //log: bar called after 2000ms
2. Parallelizing Asynchronous operations
Here’s a trivial example of the pending operations pattern which uses a counter to monitor multiple parallel async operations:
var pendingOps = 0; function allDone (){ pendingOps--; log("Pending calls: ", pendingOps); if(pendingOps === 0){ log("All done"); } } pendingOps++; delay(allDone, 4000); pendingOps++; delay(allDone, 2000); // -- After 2 seconds -- //log: Pending calls: 1 // -- After 4 seconds -- //log: Pending calls: 0 //log: All done
Although this approach works, it requires counter changes for every new async operation. It is also risky – a wrong counter change and you get a difficult bug. Alternative approach? Promises of course!
The Promise.all ‘static’ method takes an array of promises and only resolves when all of its promises are resolved. The then function is called with an array of values corresponding to the resolution states of the input promises and in their original order.
p = new Promise(function(resolve,reject){ delay(resolve, 4000); }); p2 = new Promise(function(resolve,reject){ delay(resolve, 2000); }); Promise.all([p,p2]).then(function(values){ log(values); }); // -- After 4 seconds --- //log: [4000,2000]
An iterable must be passed to the Promise.all function otherwise it’ll cause an error.
//fails because input is not iterable Promise.all(p, p2).then(function(values){ log(values); }).catch(function(error){ log(error); }); //log: TypeError: invalid_argument
3. Ordering Asynchronous operations
Web developers sometimes need to make an async request using the results of an earlier async request. For example, retrieving the id of a user and then using that to get more information. This is implemented by using a callback inside a callback, something like the timeout example below:
var val = null; setTimeout(function() { val = "val set in first call"; log(val); setTimeout(function() { if(val){ log("Inside second call"); } }, 2000); }, 2000); //log: val set in first call //log: Inside second call
The example above shows two nested callbacks, the first one returns and sets a value which is then used in the second callback – this is typical of what happens in a lot of AJAX requests. However this approach is brittle, requires careful checks to avoid bugs and can be broken if async operations complete out of order. Just as you expected, promises can help…
p1 = new Promise(function(resolve,reject){ delay(resolve, 4000); }); p1.then(function (val) { log("P1 done after "+ val + " ms"); p2 = new Promise(function(rslv,rjct){ delay(rslv, 2000); }); return p2; }).then(function (val) { log("P2 done after "+ val + "ms"); }); //-- 4 second wait -- //log: P1 done after 4000ms // -- 2 second wait -- //log: P2 done after 2000ms
The second then call waits until the new Promise is fulfilled (or rejected) before it is called. If a non-promise value is passed in, it will execute immediately, however since a promise was passed to it, it ‘waited’ for the promise to fulfill before executing.
And that’s about it! Insha Allaah, the next post should be about the compatibility issues between jQuery Defereds and the Promise/A+ specification.
One thought on “3 Ways to start using promises”