Initially, dealing with asynchronous tasks is a bit tricky. A lot of times, you want to run code only after a task completes.
If you have a task, and you don't know it will finish, how do you ensure that it's done before running other code that's dependent on it? →
Assuming we have a function called get that retrieves a url… we tend to want to do this →
const data = get(url);
parseResult(data);
But if our get is asynchronous, we can't guarantee that get finishes before parseResult is called (so callback functions it is) →
get(url, function(data) {
parseResult(data);
});
Ok. We get asynchronous tasks… and we understand that:
So… what happens if we have async tasks that are dependent on other async tasks? For example:
Let's assume that we have our get function:
Using our imaginary get function, what would this look like? →
A tiny pyramid. ▲ ▲ ▲ ▲ ▲
We use a bunch of nested callbacks… (the pyramid is the white space to the left).
get(url, function(data) {
const urlTwo = parseResult(data);
get(urlTwo, function(data) {
const urlThree = parseResult(data);
get(urlThree, function(data) {
console.log("Aaaand we're done");
});
});
});
Create 3 json files that each have an object with a url
property holding the url of another json file. Then retrieve these files one by one… →
data
within public
tango.json
: { "url":"http://localhost:3000/data/uniform.json" }
uniform.json
: { "url":"http://localhost:3000/data/victor.json" }
victor.json
: {}
tango.json
Oh hello scrollbars. This won't even fit on this slide.
const url = 'http://localhost:3000/data/tango.json';
req1 = new XMLHttpRequest();
req1.open('GET', url, true);
req1.addEventListener('load', function() {
console.log('loading req1');
if(req1.status >= 200 && req1.status < 400) {
console.log(req1.responseText);
const data1 = JSON.parse(req1.responseText)
console.log(data1.url);
req2 = new XMLHttpRequest();
req2.open('GET', data1.url, true);
req2.addEventListener('load', function() {
console.log('loading req2');
if(req2.status >= 200 && req2.status < 400) {
console.log(req2.responseText);
const data2 = JSON.parse(req2.responseText)
console.log(data2.url);
req3 = new XMLHttpRequest();
req3.open('GET', data2.url, true);
req3.addEventListener('load', function() {
console.log('loading req3');
if(req3.status >= 200 && req3.status < 400) {
console.log(req3.responseText);
console.log('done');
}
});
req3.send();
}
});
req2.send();
}
});
req1.send();
Oof. Apologies for making your eyes bleed.
get(url, cb)
extractURL(json)
So… this function will retrieve a url, and when it gets a response, it'll call the callback with the response text.
function get(url, cb) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
console.log('loading req');
if(req.status >= 200 && req.status < 400) {
console.log(req.responseText);
cb(req.responseText);
}
});
req.send();
}
This one's simple
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
Ah. Much nicer.
const url = 'http://localhost:3000/data/tango.json';
get(url, function(responseText) {
const url2 = extractURL(responseText);
get(url2, function(responseText) {
const url3 = extractURL(responseText);
get(url3, function(responseText) {
console.log('done');
});
});
});
We still get a tiny pyramid, though. To get around that, we can:
Getting and extracting were repeated 3 times. Why don't we just wrap this in another function? →
(this only works because we're doing the same exact thing in each level of callback nesting).
function getAndExtract(url) {
get(url, function(responseText) {
const url = extractURL(responseText);
if(url) {
getAndExtract(url);
} else {
console.log('done');
}
});
}
getAndExtract(url);
So, an alternate way to deal with this is to use an API that allows to code as if we were dealing with simple, sequential operations.
One of these APIs, Promise, is in ES6 and is actually already available on a lot of browsers
A Promise is an object that represents an asynchronous action - some operation that may or may not have been completed yet.
For example, a Promise may represent:
Again, a Promise is an object that represents an async task →
Consequently, a Promise can be in one of the following states:
To create a Promise
use the Promise
constructor: →
executor
fulfill
)reject
)
const p = new Promise(function(fulfill, reject) {
// do something async
if(asyncTaskCompletedSuccessfully) {
fulfill('Success!');
} else {
reject('Failure!');
}
});
Promise objects have a couple of methods that allow the fulfill
and reject
arguments of the executor
function to be set: →
then(fulfill, reject)
- sets both the fulfill
and reject
functionscatch(reject)
- only sets the reject
functionthen
can represent the next step to execute when a Promise
completes (either successfully or fails). →
Let's take a look at how then
works:
What is the output of this code, if any? →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
Success!
Let's take a closer look at what's happening here: →
const p = new Promise(function(fulfill, reject) {
fulfill('Success!');
});
p.then(function(val) {
console.log(val);
})
then
is a function that takes a single argument and logs out that argumentthen
sets fulfill
to the function above, so calling fulfill
results in logging out the valuethen
p.then(console.log);
The functions passed to then
are guaranteed to be executed AFTER the Promise is created. →
This is true even if it looks like fulfill
is called immediately and before then
is called!. What's the output of this code? →
const p1 = new Promise(function(fulfill, reject) {
console.log('begin');
fulfill('succeeded');
console.log('end');
});
p1.then(console.log);
begin
end
succeeded
// the fulfill function, console.log, is
// guaranteed to be called after the Promise
// is created even though it looks like fulfill
// is called between logging begin and end!
then
's Second ArgumentTo specify what happens when a Promise results in an error or if the async task fails, use then
's 2nd argument. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.then(console.log, function(val) {
console.log('ERROR', val);
});
The code above results in the following output …
ERROR did not work!
catch
You can also use the method catch
to specify the reject
function. →
const p = new Promise(function(fulfill, reject) {
reject('did not work!');
});
p.catch(function(val) {
console.log('ERROR', val);
});
then
always returns a Promise →
fulfill
function returns a Promise
, then
will return that Promise
fulfill
function returns a value, then
will return a Promise
that immediately fulfills with the return value
That sounds convoluted… Let's see some examples. →
then
return ValueStarting with a Promise
…
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
The fulfill
function passed to then
returns a Promise
, so then
returns that same Promise
object (which is assigned to p2
)
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
Because p2
is another Promise
, we can call then
on that too.
p2.then(console.log);
So the resulting output is… →
1
2
Let's make a minor modification to the code in the previous slide. Again, start with a Promise… →
const p1 = new Promise(function(fulfill, reject) {
fulfill(1);
});
This time, though, instead of fulfill
returning a Promise
, it'll return a regular value.
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
Again, let's try calling then
on p2
(but is p2
a Promise
… or will an error occur!?)
p2.then(console.log);
p2
is still a Promise
If fulfill
returns a non-Promise, then
will return a Promise
that immediately calls fulfill with the value that was returned. →
Consequently, the following two code samples return the same Promise
for p2
:
const p2 = p1.then(function(val) {
console.log(val);
return new Promise(function(fulfill, reject) {
fulfill(val + 1);
});
});
const p2 = p1.then(function(val) {
console.log(val);
return val + 1;
});
So maybe our version of get
will now just give back a Promise to wrap the async code. →
function get(url) {
return new Promise(function(fulfill, reject) {
console.log('getting ', url);
req = new XMLHttpRequest();
req.open('GET', url, true);
req.addEventListener('load', function() {
if(req.status >= 200 && req.status < 400) {
fulfill(req.responseText);
} else {
reject('got bad status code ' + req.status);
}
});
// also reject for error event listener!
req.send();
});
}
function extractURL(json) {
const data = JSON.parse(json)
console.log(data.url);
return data.url;
}
const url = 'http://localhost:3000/data/tango.json';
get(url)
.then(extractURL)
.then(get)
.then(extractURL)
.then(get)
.then(extractURL)
.then(function(val){
console.log(val);
console.log('done');
});
So, promises are kind of complicated, but in the end, they do simplify things. Some things that we didn't cover that further show the power of using the Promise API are:
Using Promise
s seemed to complicate AJAX rather than make it easier.
It's certainly tricky manually wrapping async tasks with Promises, but:
fetch
api provides a global function fetch
that allows the retrieval of a url
fetch(url)
.then(function(response) { return response.text(); })
.then(handleResponse)