Promises - HTML5 APIs - HTML5, JavaScript and jQuery (Programmer to Programmer) - 2015

HTML5, JavaScript and jQuery (Programmer to Programmer) - 2015

Part IV HTML5 APIs

Lesson 36 Promises

Throughout this book, you have made extensive use of callback functions. You have used callback functions:

· To register event listeners that fire when specific events occur

· To listen for the completion of IndexedDB operations, such as the insertion of data or the opening of the database

· When using the JavaScript filter, map, and reduce functions to process arrays

· When listening for messages from web workers

· When waiting for AJAX responses

As you can see, callback-based programming is enormously important to many JavaScript APIs, and it is impossible to gain a solid understanding of JavaScript without understanding callback functions.

Although callback-based APIs are enormously popular, they do bring their own set of problems. These problems are often referred to as “Callback hell” and stem largely from the following issues:

· It is often necessary to nest callbacks inside other callbacks, and this nesting can extend to several levels. As this happens, code can become difficult to read because it is not always obvious where each level of nesting ends.

· It is difficult to determine the behavior of an application because it is not possible to logically follow code with your eye.

· Data scoping can become difficult with callbacks. You can see this primarily in relation to the identity of this and the corresponding necessity to use bind on each callback to ensure the correct this instance was set.

This lesson examines an alternative mechanism for implementing callback functions called promises. Promises do not alleviate all of the problems mentioned, but they do help make callbacks more manageable and add other useful functionality in the process.

This lesson is optional because it is always possible to write code without promises, as you have seen so far in this book. It is, however, recommended that you complete this lesson because promises are likely to gain greater attention as more and more APIs are designed to work with them.

Working with Promises

This book has used several asynchronous APIs. You may have noticed, however, that asynchronous function calls still return a synchronous response. For instance, the following code invokes an asynchronous AJAX operation:

$.get('contacts.json')

Despite this, it returns the synchronous response shown in Figure 36.1.

image

Figure 36.1

Clearly, this response cannot be the response from the web server because it was not received asynchronously, so you may be wondering what this object is.

This object is referred to as a “promise.”

Note

Not all asynchronous APIs have been modified to generate promises. For this reason, this lesson will focus on the jQuery AJAX library that was retrofitted to operate with promises.

Promises provide a mechanism for interacting with an asynchronous process because they model the flow of the underlying operation through its lifecycle. Promises always start with a status of pending. From this they will transition to one of two other statuses:

· resolved: The underlying process has completed successfully.

· rejected: The underlying process has failed.

Once a promise has moved into the state resolved or rejected, it is considered settled, and will not change state again.

Once you have a reference to a promise, you can request to be notified when the promise enters a specific state. For instance, consider the following code:

var promise = $.get('contacts.json');

promise.done(function(data) {

console.log('First callback invoked');

});

promise.done(function(data) {

console.log('Second callback invoked');

});

This code requests contacts.json and then registers two callbacks that will be invoked when the promise is resolved. If you run this code, you will notice that both callback functions are executed, one after the other.

This code immediately highlights one advantage of promises: It is possible to register more than one callback for each state

It is also possible to add multiple fail callbacks:

var promise = $.get('unknown.json');

promise.fail(function(data) {

console.log('First callback invoked');

});

promise.fail(function(data) {

console.log('Second callback invoked');

});

This code requests a non-existent file; therefore, it enters the rejected state and invokes all the failure callbacks.

It is also possible to create an explicit pipeline of operations that should be performed on a response via the then method. This is useful because it allows the response to be modified as it flows through the pipeline.

For instance, in the following scenario, the first callback filters the response so that it contains only contacts with a companyName value of 3. It then returns this modified response, which is then passed automatically to other callbacks in the pipeline:

$.get('contacts.json').then(function(data) {

console.log('First callback invoked with ' + data.length + ' contacts');

data = data.filter(function(c) {

return c.companyName == '3';

});

return data;

}).then(function(data) {

console.log('Callback invoked with ' + data.length + ' contacts');

});

In this case, the second callback does not know or care that the data it is receiving has already been processed by another callback. This therefore provides a convenient mechanism for pre-processing data.

This code prints the following:

First callback invoked with 3 contacts

Callback invoked with 2 contacts

One final benefit of working with promises is that it is possible to register callbacks that are invoked when more than one promise reaches a specific state. In order to demonstrate this, create a new file called contacts2.json and add a different set of contacts to it.

You can now write code that performs two separate requests for these two separate resources, and invokes the callback only if both requests succeed via the $.when function:

var promise1 = $.get('contacts.json');

var promise2 = $.get('contacts2.json');

$.when(promise1, promise2).done(function(data1, data2) {

console.log('data1 contains ' + data1.length + ' contacts');

console.log('data2 contains ' + data2.length + ' contacts');

});

This can be extremely useful if the two sets of data contain interdependencies. Without callbacks, it would typically be necessary to place the second request in the success callback of the first request.

Creating Promises

Not only can you use promises created by other libraries, but you can write your own APIs that generate promises. This allows the clients of these APIs to use all the techniques outlined in the previous section, and it effectively means that your API gains extra capabilities without your having to do anything.

Promises are a useful addition to any API that performs operations that are (or can be) asynchronous.

Note

The client of an API is any code that invokes its functions or methods; in many cases, this will be your own code. If you are developing libraries, on the other hand, you may have no idea who will be the clients of your code.

In this section, you will create a function call for reading contacts from the web server. This function will have a catch, however. It will only invoke the server the first time it is invoked; from then on it will simply return the response that it received on the first invocation.

This approach is referred to as caching and is often used to improve the performance of a web application.

This functionality is interesting because it only needs to behave asynchronously the first time it is invoked. On subsequent invocations it will have a response that can be returned immediately. As you will see, promises are an excellent candidate for implementing this functionality.

Because your code needs to remember state (the contacts read from the server on the first invocation), you will create a new module in a new JavaScript file called find.contacts.js with the following basic structure:

findContacts = function() {

var contacts = null;

return function() {

console.log('this is where the logic goes');

}

}();

The outer anonymous function is executed as soon as this code is loaded via the () on the final line. Therefore, as soon as this code is loaded, the findContacts variable contains a reference to a function, which in turn can access the contacts variable via a closure.

You can now add the following implementation:

findContacts = function() {

var contacts = null;

return function() {

var deferred = $.Deferred();

if (contacts) {

console.log('Returning data from the cache');

deferred.resolve(contacts);

return deferred.promise();

} else {

var promise = $.get('contacts.json');

console.log('Returning data from the server');

promise.done(function(data) {

contacts = data;

deferred.resolve(contacts);

});

return deferred.promise();

}

}

}();

The code always begins by creating an instance of a deferred object:

var deferred = $.Deferred();

It is this object that allows you to not only create promises, but also control the lifecycle of these promises by transitioning them from one state to another.

The preceding code contains two blocks. The first block will execute if you have already read the list of contacts from the server. In this case, any promises can be set to resolved immediately; therefore, you invoke resolve on the deferred object and pass it the cached list of contacts:

deferred.resolve(contacts);

On the next line of code, you generate a promise from the deferred object, and return this from the function. Notice that in this case the promise will already be fulfilled:

return deferred.promise();

The second block of code begins by invoking the server and registering a callback to listen for the response, but in this case you also return a promise from the function. Unlike the first block, the promise is not resolved until a response is received from the server.

Because the server will respond very quickly, it is useful to simulate a slower server to appreciate the benefit of this functionality. Therefore, change the AJAX response processing to include a 5 second delay:

promise.done(function(data) {

setTimeout(function() {

contacts = data;

deferred.resolve(contacts);

}, 5000);

});

You can now use this function. Begin by importing the JavaScript file into contacts.html, ensuring that it is not imported before the jQuery library.

<script src="find.contacts.js"></script>

You can now change the event listener associated with the Import from the server button in contacts.js, as follows:

$(screen).find('#importFromServer').click(function(evt) {

var promise = findContacts();

promise.done(function(data) {

console.log('Data has been retrieved');

console.log(data);

});

});

Notice that the event listener does not know how the findContacts function operates; it only cares that it produces a promise. The code then registers a callback for when this promise reaches its resolved state.

If you now refresh the web page and open the console, you can try out the functionality. The first time you press the button, you should see the following immediately printed to the console:

Returning data from the server

After a further 5 seconds, you should see the following:

Data has been retrieved

If you then press the button again, you should see the following lines printed immediately:

Returning data from the cache

Data has been retrieved

Notice in this case that you have registered a callback with a promise that has already been fulfilled. When you do this, your callback simply executes immediately. It is even possible to register additional callbacks with promises that are fulfilled or rejected, and these are simply invoked immediately.

Try It

In this Try It, you will create a generic function for reading files and implement it so that it is compatible with promises. Although the FileReader object operates asynchronously, it does not produce a promise; therefore, it is not possible to use it with other utility functions such as $.when.

Lesson Requirements

You will need a text editor and Chrome to complete this Try It.

Step-by-Step

1. Open contacts.js, and start by adding a global function called readFileWithPromise immediately below the bind function. This should accept a single parameter, which is the file to read.

2. Create an instance of $.Deferred and assign it to a variable called deferred.

3. Create an instance of FileReader and register an onload callback. Inside this callback, you should resolve the deferred object and pass it the contents of event.target.result.

4. Read the file specified as the parameter using readAsText. Remember that this will cause the onload callback to be invoked when it completes, and will pass the contents of the file as a JavaScript string.

5. Return a promise from the function.

6. Change the event listener invoked when a file is selected so that it invokes readFileWithPromise and assigns the result to a variable called promise.

7. Add a success listener to the promise and accept a single parameter, which will be the textual content of the file.

8. Modify the code of the callback so that the text is parsed and processed as it was previously.

My version of readFileWithPromise looks like this:

function readFileWithPromise(file) {

var deferred = $.Deferred();

var reader = new FileReader();

reader.onload = function(evt) {

deferred.resolve(evt.target.result);

}

reader.readAsText(file);

return deferred.promise();

}

My callback listener looks like this:

$(screen).find('#importJSONFile').change(function(evt) {

var promise = readFileWithPromise(event.target.files[0]);

promise.done(function(data) {

var contacts = JSON.parse(data);

for (var i = 0; i < contacts.length; i++) {

this.store(contacts[i]);

}

location.reload();

}.bind(this));

}.bind(this));

Reference

Please go to the book's website at www.wrox.com/go/html5jsjquery24hr to view the video for Lesson 36, as well as download the code and resources for this lesson.