The traditional way to deal with asynchronous tasks in smart pascal are
callbacks; call a method, give it a function reference to execute once that method is done. For example, say I want to run code asynchronous that:
a) fetch the person object, after half a second;
b) create the message;
c) change the message; and
d) display the message.
type
TCallbackFn = procedure(d: variant; f: variant);
var person := JSON.parse('{"firstName": "Warley", "lastName":"Alex", "favoriteTeam":"Cruzeiro"}');
var api = CLASS
getPerson := lambda(callback: TCallbackFn)
w3_SetTimeout(lambda()
callback(null, person);
end, 500);
end;
getMessage := lambda(person: variant; callback: TCallbackFn)
w3_SetTimeout(lambda()
callback(null, person.firstName + ' ' + person.lastName + ' likes the ' + person.favoriteTeam);
end, 500);
end;
mapMessage := lambda(message: variant): variant
Result := message + ', the 2nd worst team in the Brazilian footbal league.';
end;
END;
{ using callback approach }
api.getPerson(lambda(err, person)
api.getMessage(person, lambda(err, message)
console.log(api.mapMessage(message));
end);
end);
{ Warley Alex likes the Cruzeiro, the 2nd worst team in the Brazilian footbal league.}
This is pretty neat, it is often much easier to read inline anonymous functions, as in the example. It will also spare you from having to find a name for all the callbacks. In such cases, using inline anonymous functions is even more preferable.
But, it has some drawbacks; for one, combining or chaining multiple asynchronous processes is tricky; it either leads to a lot of boilerplate code, or what's known as
callback hell.
Callback hell is any code where the use of function callbacks in async code becomes
obscure or difficult to follow. Generally, when there is more than one level of indirection, code using callbacks can become harder to follow, harder to refactor, and harder to test. A code smell is multiple levels of indentation due to passing multiple layers of function literals.
There is an alternative to dealing with asynchronous code in smart pascal: using
Promises.
Promises are an alternative to callbacks for delivering the results of an asynchronous computation.
They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions. Promises help deal with the callback nesting and with the exception handling.
The Simplest Use Case
Let's begin our promise implementation as simple in Smart Mobile Studio.
Just create an unit and define this externals classes:
type
JDeferred = class;
TCallback = procedure(Value: Variant);
TCallbackFn = procedure(d: variant; f: variant);
TJDeferred_object_new_fn_ = function (d: TCallback): variant;
TJDeferred_object_new_fn = procedure (resolve: TCallback; reject: TCallback);
TPromiseCallback = function(Value: Variant): Variant;
TEventHandler = function(event: variant): Variant;
JPromise = class external "Promise"
constructor create(fn: TJDeferred_object_new_fn_{ = nil}); overload;
constructor create(resolve: TJDeferred_object_new_fn_; reject: TJDeferred_object_new_fn_); overload;
constructor create(fn: TJDeferred_object_new_fn); overload;
function always(alwaysCallbacks: array of variant): JPromise;
function done(doneCallbacks: array of variant): JPromise; overload;
function done(doneCallbacks: variant): JPromise; overload;
function fail(failCallbacks: array of variant): JPromise;
function progress(progressCallbacks: array of variant): JPromise;
function state(): string;
function &then(doneCallbacks: variant; failCallbacks: variant = undefined; progressCallbacks: variant = undefined): JPromise;
function &then(onFulfilled: TPromiseCallback = nil): JPromise; overload;
function &then(onFulfilled: TPromiseCallback; onRejected: TPromiseCallback): JPromise; overload;
function catch(rejectCallback: Variant = nil): JPromise; overload;
function catch(rejectCallback: TPromiseCallback): JPromise; overload;
class function promise(target: Variant): JPromise;
end;
type
JDeferred = class external "Promise"(JPromise)
function notify(args: array of variant): JDeferred;
function notifyWith(context: variant; args: array of variant): JDeferred;
function reject(args: array of variant): JDeferred; overload;
function reject(args: variant): JDeferred; overload;
function reject(args: TEventHandler): JDeferred; overload;
function rejectWith(context: variant; args: array of variant): JDeferred;
function resolve(args: array of variant): JDeferred; overload;
class function resolve(value: variant = nil): JPromise; overload;
function resolveWith(context: variant; args: array of variant): JDeferred;
function all(iterable: Array of Variant): JPromise;
function race(iterable: Array of Variant): JPromise;
end;
{ global external functions }
function Promise : JDeferred; external 'Promise' property;
function Error(message: variant): variant; external 'Error';
function document: variant; external "document" property;
function window : Variant; external 'window' property;
function &typeof(obj:variant): variant; overload; external "typeof";
The following function returns a result asynchronously, via a Promise:
function asyncFunction(): JPromise;
begin
Result := new JPromise(
function (resolve: TCallback): variant
begin
w3_SetTimeout(procedure()
begin
var value := 42;
resolve('Async Hello world: ' + IntToStr(value) );
end, 16);
end,
function(reject: TCallback): variant
begin
// case rejected
end);
end;
{*
then() always returns a Promise, which enables you to chain method calls:
Furthermore, note how catch() handles the errors of the asynchronous function asyncFunction().
*}
asyncFunction()
.then(procedure (value: variant)
begin
console.log(value); // => 'Async Hello world'
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
Chaining Promises
The real key here is that then() is returning a new promise. Since promises capture the notion of asynchronicity in an object, we can chain them, map them, have them run in parallel or sequential, all kinds of useful things.
Code like the following is very common with promises:
asyncFunction()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
What value does the second promise resolve to? It receives the return value of the first promise.
In other words, it’s what was passed into the first call to then(). The return value of that first handler is used to resolve the second promise. Thus
chaining is accomplished!
The
asyncFunction is returning a promise, as evidenced by the call to then(), "then() always returns a promise" but the result of that first then must also be a promise, as we call then() again (and yet again!)
That’s exactly what happens, if we can convince then() to return a promise, things get more interesting.
Since then() always returns a new promise object, there will always be at least one promise object that gets created, resolved and then ignored.
Example:
function asyncFunction(): JPromise;
begin
Result := new JPromise(
function (resolve: TCallback): variant
begin
w3_SetTimeout(procedure()
begin
var value := 42;
resolve('getting_The_Data: ' + IntToStr(value) );
end, 16);
end,
function(reject: TCallback): variant
begin
// case rejected
end);
end;
asyncFunction()
.then(function (value: variant): variant
begin
console.log('first result: '+ value); // => 'getting_The_Data: 42'
exit('filter_The_Data');
end)
.then(function (value: variant): variant
begin
console.log('second result: '+ value); // => 'filter_The_Data'
exit('process_The_Data'); // explicitly returning the string "process_The_Data"
end)
.then(function (value: variant): variant
begin
console.log('third result: '+ value); // => 'process_The_Data'
exit('display_The_Data');
end)
.then(function (value: variant): variant
begin
console.log('forth result: '+ value); // => 'display_The_Data'
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
(* the output is:
first result: getting_The_Data: 42
second result: filter_The_Data
third result: process_The_Data
forth result: display_The_Data
*)
What if in the above example, we wanted all the results in the end in one go.
Executing asynchronous functions in parallel
Promises always resolve to one value. If you need to pass more than one value along, you need to create a
multi-value in some fashion (an array, an object, concatinating strings, etc).
With chaining, we would need to manually build up the result ourself. We can create a function to return a promise object such as:
["getting_The_Data: 42","filter_The_Data","process_The_Data","display_The_Data"]
Example:
function asyncFunction(): JPromise;
begin
Result := new JPromise(
function (resolve: TCallback): variant
begin
w3_SetTimeout(procedure()
begin
var value := 42;
resolve('getting_The_Data: ' + IntToStr(value) );
end, 16);
end,
function(reject: TCallback): variant
begin
// case rejected
end);
end;
var results: array of variant;
asyncFunction()
.then(function (value: variant): array of variant
begin
results := [value];
results.push('filter_The_Data');
exit(results);
end)
.then(function (value: variant): array of variant
begin
results.push('process_The_Data');
exit(results);
end)
.then(function (value: variant): array of variant
begin
results.push('display_The_Data');
exit(results);
end)
.then(function (value: variant): variant
begin
console.log( JSON.stringify(variant(results)) );
{* for var i := results.Low to results.High do
console.log(results[i]); *}
{*for var j in results do
console.log(j);*}
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
The Callback is Optional
The callback to then() is not strictly required. If you leave it off, the promise resolves to the same value as the
previous promise.
NOTE: If there is no callback in method then(), it simply resolves the promise and exits. The value is still the value of the previous promise.
Example:
function asyncFunction(): JPromise;
begin
Result := new JPromise(
function (resolve: TCallback): variant
begin
w3_SetTimeout(procedure()
begin
var value := 42;
resolve('getting_The_Data: ' + IntToStr(value) );
end, 16);
end,
function(reject: TCallback): variant
begin
// case rejected
end);
end;
asyncFunction()
.then()
.then()
.then()
.then(function (value: variant): variant
begin
console.log('forth result: '+ value); // => 'forth result: getting_The_Data: 42'
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
Returning JSON object Inside the Chain
What if one of the resolved values is a JSON? For example:
function doSomethingElse(value: variant): variant;
begin
result := JSON.parse('{"name":"'+value+'", "id":123}');
end;
asyncFunction()
.then(function (value: variant): variant
begin
console.log('first result: '+ value); // =>
{ doSomethingElse returns a promise }
exit( doSomethingElse(value) );
end)
.then(function (finalResult: variant): variant
begin
console.log(finalResult); // => Object {name: "getting_The_Data: 42", id: 123}
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
Rejecting Promises (error handling support)
When something goes wrong during the course of a promise, it needs to be rejected with a reason.
How does the caller know when this happens?
They can find out by passing in a second callback to then()
The promise will transition from pending to either resolved or rejected, never both.
In other words, only one of the above callbacks ever gets called.
Promises enable rejection by means of reject(), the evil twin of resolve().
Here is asyncFunction() with error handling support added
function asyncFunction(): JPromise;
begin
result := JPromise.create(function(resolve: TCallback; reject: TCallback): variant
begin
var res := doSomethingElse();
if(res.error = true) then
begin
reject(res.error);
end else
resolve(res.value);
end);
end;
asyncFunction()
.then(function (value: variant): variant
begin
console.log('Success! '+ value);
//console.log(value.error);
end,
function(error: variant): variant
begin
console.log('Uh oh '+ error);
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
When using promises, it’s very easy to omit the error callback.
But if you do, you’ll never get any indication something went wrong. At the very least, the final promise in your chain should have an error callback.
Unexpected Errors Should Also Lead to Rejection
So far our error handling only accounts for known errors. It’s possible an unhandled exception will happen,
completely ruining everything. It’s essential that the promise implementation catch these exceptions and reject
accordingly. It’s possible for a misunderstanding of promises to lead to completely swallowed errors! This trips people up a lot.
Example:
function asyncFunction(): JPromise;
begin
result := JPromise.create(
procedure(resolve: TCallback; reject: TCallback)
begin
var badJson := "<div>uh oh, this is not JSON at all!</div>";
resolve(badJson);
end);
end;
asyncFunction()
.then(function (jsonData: variant): variant
begin
var obj := JSON.parse(jsonData);
console.log(obj);
exit(obj);
end,
function (error: variant): variant
begin
console.log("an error occured: "+ error);
end);
What is going to happen here? Our callback inside then() is expecting some valid JSON.
So it naively tries to parse it, which leads to an exception. But we have an error callback, so we’re good, right?
Nope. That error callback will not be invoked! If you run this example, you will get no output at all. No errors, no nothing. Pure chilling silence.
Why is this? Since the unhandled exception took place in our callback to then(), it is being caught inside of handle().
This causes handle() to reject the promise that then() returned, not the promise we are already responding to, as that promise has already properly resolved.
Always remember, inside of then()'s callback, the promise you are responding to has already resolved.
The result of your callback will have no influence on this promise.
If you want to capture the above error, you need an error callback further downstream, for instance:
asyncFunction()
.then(function (jsonData: variant): variant
begin
var obj := JSON.parse(jsonData);
console.log(obj);
exit(obj);
end)
.then(nil, function (error: variant): variant
begin
console.log("an error occured: "+ error);
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
Now we will properly log the error. In my experience, a potentially better solution would be implement a catch method to capture errors.
asyncFunction()
.then(function (jsonData: variant): variant
begin
var obj := JSON.parse(jsonData);
console.log(obj);
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
Loading Images on-the-fly
Now, let's create a more realistic example using promise in SMS.
Let's create a function that dynamically loads an image based on the image URI:
Remember that it is passed an anonymous function with two parameters:
a) resolve() method that you call at some point to set the state of the promise to fulfilled,
b) reject() method to set it to rejected instead.
A Promise object starts out with a state of pending, to indicate the asynchronous code it's monitoring has neither completed (fulfilled) or failed (rejected).
Example:
function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
procedure(resolve: TCallback; reject: TCallback)
var img: JHTMLImageElement;
begin
img := JHTMLImageElement(Document.createElement('img'));
JGlobalEventHandlers(img).onload := lambda
resolve(url)
end;
JGlobalEventHandlers(img).onerror := lambda
reject(url)
end;
img.src := url;
end);
end;
The
getImage() function returns a Promise object that keeps track of the state of the image load.
Its promise object goes from the initial state of "pending" to either fulfilled or rejected eventually depending on the outcome of the image load.
Notice how we've passed the URI of the image to both the resolve() and reject() method of Promise;
this could be any data you wish to be processed further depending on the outcome of the task.
Ok, at this point Promises may just seem like a pointless exercise to set some object's state to indicate the status of a task. But as you'll soon see, with this mechanism comes the ability to easily and intuitively define what happens next once the task is completed.
The then() and catch() methods
Whenever you instantiate a Promise object, two methods:
a) then() method and
b) catch() method become available to decide what happens next after the conclusion of an asynchronous task.
Take a look at the below:
Here as soon as "test.png" has loaded, we specify that the image be shown inside the "OBJ3" DIV.
The original getImage() function returns a Promise object, so we can call then() on it to specify what happens when the request has been resolved. The URL of the image we passed into the resolve() function when we created getImage() becomes available as the parameter inside the then() function.
What happens if the image failed to load? The then() method can accept a 2nd function to deal with the rejected state of the Promise object:
Example:
getImage('test.png')
.then(function (successurl: variant): variant
begin
document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
end,
function(errorurl: variant): variant
begin
console.log("error loading "+ errorurl);
end);
In such a construct, if the image loads, the first function inside then() is run, if it fails, the 2nd instead.
Of course, we can also handle errors using the catch() method instead:
Example:
getImage('test.png')
.then(function (successurl: variant): variant
begin
document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
end)
.catch(procedure (errorurl: variant)
begin
console.log("error loading "+ errorurl);
end);
{* Calling catch() is equivalent to calling then(undefined, function),
so the above is the same as: *}
getImage('test.png')
.then(function (successurl: variant): variant
begin
document.getElementById('OBJ3').innerHTML := '<img src="' + successurl + '" />';
end)
.then(nil, function (errorurl: variant): variant
begin
console.log('Error loading ' + errorurl)
end);
Fetching and displaying images sequentially
Lets say we have an
array of images we want to load and display sequentially.
That is to say, first load and show image1, and once that's complete, go on to image2, and so on.
We'll talk about chaining promises together further below to accomplish this, but one approach is just to use
recursion to go through the list of images, calling our getImage() function each time with a then() method that
shows the current image before calling getImage() again until all of the images have been processed.
Here is the code:
function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
procedure(resolve: TCallback; reject: TCallback)
var img: JHTMLImageElement;
begin
img := JHTMLImageElement(document.createElement('img'));
JGlobalEventHandlers(img).onload := lambda
resolve(url)
end;
JGlobalEventHandlers(img).onerror := lambda
reject(url)
end;
img.src := url;
end);
end;
var doggyplayground := document.getElementById('OBJ3');
var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
procedure displayimages(images: array of variant);
begin
var targetimage := variant(images).shift(); // process doggies images one at a time
if (targetimage) then begin // if not end of array
getImage(targetimage)
.then(function(url: variant): variant // load image then...
begin
var dog := JHTMLImageElement(document.createElement('img'));
dog.setAttribute('src', url);
doggyplayground.appendChild(dog); // add image to DIV
displayimages(images); // recursion- call displayimages() again to process next image/doggy
end)
.catch(function(url: variant): variant // handle an image not loading
begin
console.log('Error loading ' + url);
displayimages(images); // recursion- call displayimages() again to process next image/doggy
end);
end;
end;
displayimages(doggies);
The
displayimages() function takes an array of images and sequentially goes through each image, by calling
images.shift(). For each image, we first call getImage() to fetch the image, then the returned Promise object's then() method to specify what happens next, in this case, add the image to the OBJ3 DIV before calling displayimages() again.
In the case of an image failing to load, the catch() method handles those instances.
The recursion stops when the doggies array is empty, after Array.shift() has gone through all of its elements.
Using recursion with JavaScript Promises is one way to sequentially process a series of asynchronous tasks.
Another more versatile method is by learning the art of chaining promises.
Chaining Promises
We already know that the then() method can be invoked on a Promise instance to specify what happens after the completion of a task. However, we can in fact chain multiple then() methods together, in turn chaining multiple promises together, to specify what happens after each promise has been resolved, in sequence. Using our trusted getImage() function to illustrate, the following fetches one image before fetching another:
Example:
function getImage(url: variant): JPromise;
begin
Result := JPromise.create(
procedure(resolve: TCallback; reject: TCallback)
var img: JHTMLImageElement;
begin
img := JHTMLImageElement(Document.createElement('img'));
JGlobalEventHandlers(img).onload := lambda
resolve(url)
end;
JGlobalEventHandlers(img).onerror := lambda
reject(url)
end;
img.src := url;
end);
end;
getImage('dog1.png')
.then(function (successurl: variant): variant
begin
console.log(successurl+ ' fetched!');
exit( getImage('dog2.png') );
end)
.then(function (url: variant): variant
begin
console.log(url+ ' fetched!');
end)
.catch(procedure (error: variant)
begin
console.log(error);
end);
This fetches "dog2.png" and returns a Promise object. By returning a Promise object inside then(), the next then() waits for that promise to resolve before running, accepting as its parameter the data passed on by the new Promise object. This is the key to chaining multiple promises together- by returning another promise inside the then() method.
Note that we can also simply return a static value inside then(), which would simply be carried on and executed immediately by the next then() method as its parameter value.
With the above example we still want to account for an image not loading, so we'll include the catch() method as well:
Example:
getImage('baddog1.png')
.then(function (successurl: variant): variant
begin
console.log(successurl+ ' fetched!');
end)
.catch(procedure (url: variant)
begin
console.log(url+ ' failed to load!');
end)
.then(function (url: variant): variant
begin
exit( getImage('dog2.png') );
end)
.then(function (url: variant): variant
begin
console.log(url+ ' fetched!');
end)
.catch(procedure (url: variant)
begin
console.log(url+ ' failed to load!');
end);
{ OUTPUT
baddog1.png failed to load!
dog2.png fetched!
}
Recall that catch() is synonymous with then(undefined, functionref), so after catch() the next then() will still be executed. Notice the organization of the then() and catch() methods- we put the return of the next promise object (or link in the chain) inside its own then() method, after the outcome of the previous promise is completely accounted for via the then() and catch() method proceeding it.
If you wanted to load 3 images in succession, for example, we could just add another set of then() then() catch() to the above code.
Creating a sequence of Promises
Ok, so we know the basic idea of chaining promises together is to return another promise inside the then() method. But manually chaining promises together can quickly become unmanageable. For longer chains, what we need is a way to start with an empty Promise object and programmatically pile on the desired then() and catch() methods to form the final sequence of promises.
We can create a blank Promise object that's resolved to begin with with the line:
var resolvedPromise := JDeferred.resolve();
var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
variant(doggies).forEach(
procedure(targetimage: variant)
begin
resolvedPromise
.then(function(): variant
begin
exit( getImage(targetimage) );
end)
.then(function(url: variant): variant
begin
console.log(url + ' fetched!');
end)
.catch(function(err: variant): variant
begin
console.log(err + ' failed to load!');
end);
end);
{
Console log:
dog1.png fetched
dog2.png fetched!
dog3.png fetched!
dog4.png fetched!
dog5.png fetched!
}
First we create a resolved Promise object called sequence, then go through each element inside the doggies[] array with forEach(), adding to sequence the required then() and catch() methods to handle each image after it's loaded. The result is a series of then() and catch() methods attached to sequence, creating the desired timeline of loading each image one at a time.
In case you're wondering, instead of using forEach() to cycle through the image array, you can also use a simple for loop instead, though the result may be more than you had bargained for:
var resolvedPromise := JDeferred.resolve();
var doggies : Array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
for var i:= 0 to doggies.length - 1 do
begin
var closure := lambda
var capturedindex := i; // capture i at each step of loop
resolvedPromise
.then(function(): variant
begin
exit( getImage(doggies[capturedindex]) );
end)
.then(function(url: variant): variant
begin
console.log(url + ' fetched!');
end)
.catch(function(err: variant): variant
begin
console.log('Err loading '+ err);
end);
end;
closure(); // invoke this function immediately
end;
{
Console log:
dog1.png fetched
dog2.png fetched!
dog3.png fetched!
dog4.png fetched!
dog5.png fetched!
}
Inside the for loop, to properly get the value of i at each step and pass it into then(), we need to create an outer closure to capture each value of i. Without the outer closure, the value of i passed into then() each time will simply be the value of i when it's reached the end of the loop, or
doggies.length - 1.
Creating an array of promises
Instead of chaining promises together, we can also create an array of promises. This makes it easy to do something after all of the asynchronous tasks have completed, instead of after each task.
For example, the following uses getImage() to fetch two images and store them as an array of promises:
var doggypromises : array of variant = [getImage('dog1.png'),
getImage('dog2.png'),
getImage('dog3.png'),
getImage('dog4.png'),
getImage('dog5.png')];
var resolvedPromise := JDeferred.all( doggypromises );
resolvedPromise
.then(function(urls: variant): variant
begin
console.log(urls); // logs ["dog1.png", "dog2.png", "dog3.png", "dog4.png", "dog5.png"]
end)
.catch(function(err: variant): variant
begin
console.log(err);
end);
Promise.all() takes an iterable (array or array-like list) of promise objects, and waits until all of those promises have been fulfilled before moving on to any then() method attached to it. The then() method is passed an array of returned values from each promise.
So what happens if one of the promises inside the array doesn't resolve (is rejected)? In that case the entire then() portion is ignore, and catch() is executed instead. So in the above scenario, if one or more of the images fails to load, it only logs an array of images that failed to load inside catch().
Displaying images when they have all been fetched (Fetch and display images at all once):
It's high time now to see an example of showing off all the doggies when they have been fetched, instead of one at a time. We'll use Array.map() to ease the pain in creating a promise array:
var doggies : array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
var doggypromises := doggies.map(getImage); // call getImage on each array element and return array of promises
var doggyplayground := document.getElementById('OBJ3');
var resolvedPromise := JDeferred.all( doggypromises );
resolvedPromise
.then(function(urls: variant): variant
begin
for var i:=0 to urls.length - 1 do
begin
var dog := document.createElement('img');
dog.setAttribute('src', urls[i]);
doggyplayground.appendChild(dog);
end;
end)
.catch(function(err: variant): variant
begin
console.log(err);
end);
Array.map() iterates through the original array and calls getImage() on each element, returning a new array using the return value of getImage() at each step, or a promise. The result is twofold- each image gets fetched, and in turn we get back an array of corresponding promises. Then, we put Promise.all() to work, passing in doggypromises to show all the images at once:
Fetch images all at once, but display then in sequence as each one becomes ready
(fetch images all at once in parallel, but show them sequentially):
Finally, as if the dogs haven't been paraded enough, lets introduce them to the doggy park in an optimized manner, not one by one, not all at once, but the best of both words. We'll fetch all of the images at once to take advantage of parallel downloading in browsers, but show them in sequence as each one becomes available (fetched).
This minimizes the time the doggies show up while still showing them in orderly sequence.
To do this, we just have to do two things we already know- create an array of promises to fetch all images at once (in parallel), then create a sequence of promises to actually show each image one at a time:
var doggies : array of variant = ['dog1.png', 'dog2.png', 'dog3.png', 'dog4.png', 'dog5.png'];
var doggypromises := doggies.map(getImage); // call getImage on each array element and return array of promises
var doggyplayground := document.getElementById('OBJ3');
var resolvedPromise := JDeferred.resolve();
variant(doggypromises).forEach(
procedure(curPromise: variant)
begin
resolvedPromise
.then(function(): variant
begin
exit( curPromise );
end)
.then(function(url: variant): variant
begin
var dog := document.createElement('img');
dog.setAttribute('src', url);
doggyplayground.appendChild(dog);
end)
.catch(function(err: variant): variant
begin
console.log(err + ' failed to load!');
end);
end);
Note that to create our sequence of promises this time, we iterate through the array of promises generated by Array.map(), and not the images array directly. This allows us to create the chain of promises without having to call getImage() each time again, which was done when we decided to fetch all images at once using Array.map() already.
Here's an example I re-created recently using promises with SMS: