本文是 ECMAScript 2015 原生异步方法 Promise 的学习笔记。网上课程由 Udacity + Google 提供,老师是卡梅伦·皮特曼(Cameron Pittman)。
学习笔记分为 8 个部分:
- callbacks vs thens
- Promise 中的 4 个状态
- Fulfilled (Resolved)
- Rejected
- Pending
- Settled
- Promise 时间线
- Promise 语法
- Promise 中的 this
- Fetch API
- 错误处理
- Chaining
以下是全文:
Callbacks vs Thens
"The Promise object is used for deferred and asynchronous computations." — MDN
What is asynchronous work?
Asynchronous work happens at an unknown or unpredictable time. Normally code is synchronous: one statement executes, there is a guarantee that the next statement executes immediately afterwards. This is ensured by the JavaScript threading model that for all intents and purposes, JavaScript runs in a single timeline. For example:
// Only one timeline to run these two lines of code, one after another.
var planetName = "Kepler 22 b";
console.log(planetName); // Kepler 22 b
Asynchronous code is not guaranteed to execute in a single unbroken timeline. The complete time of asynchronous operations is not predictable. For example:
// Even the first request is sent first, we can not assume the first one will be full-filled first.
var file1 = get('file1.json');
var file2 = get('file2.json');
console.log(file1); // undefined
console.log(file2); // undefined
So what is the best way to handle asynchronous code?
function loadImage(src, parent, callback) {
var img = document.createElement('img');
img.src = src;
img.onload = callback;
parent.appendChild(img);
};
Callbacks are the default JavaScript technique for handling asynchronous work. Pass the callback function to another function and then call the callback function at some later time when some conditins have been met.
How do you handle errors?
Error handling needs to be considered since there is chance to fail because of network, server no respond, wrong JSON format, and so on.
How do you create a sequence of work?
There is one scenario that leads to something called the Pyramid of Doom/Callback Hells.
loadImage('above-the-fold.jpg', imgContainer, function(){
loadImage('just-below-the-fold.jpg', imgContainer2, function(){
loadImage('farther-down.jpg', imgContainer3, function(){
loadImage('this-is-getting-ridiculous.jpg', imgContainer4, function(){
loadImage('abstract-art.jpg', imgContainer5, function(){
loadImage('egyptian-pyramids.jpg', imgContainer6, function(){
loadImage('last-one.jpg', imgContainer7);
}
}
}
}
}
})
What's the problem with this way of writing code?
It's hard to write. It looks ugly. And most important: it's incredibly frustrating to debug.
Let's try another way of writing this.
var sequence = get('example.json')
.then(doSomething)
.then(doSomethingElse);
Writing with Promise makes codes easy to read and understand.
Four States of Promise
Fulfilled (Resolved)
It worked. :)
Rejected
It didn't work. :(
Pending
Still waiting...
Settled
Something happened! Settled means that the promise has either fulfilled or rejected.
Promise Timeline
For the left side of setting event listener after event fires: When we set the event listener after event fires, nothing will happen.
For the right side of Pormise: Even the action is set after the Promise has been resolved, it will execute.
Another difference between these two methods is that event listener can be fired many times, but a Promise can be settled only once. For example:
new Promise(function(resolve, reject) {
resolve('hi'); // works
resolve('bye'); // cannot happen a second time
})
Note that Promises execute in the main thread, which means that they are still potentially blocking:
If the work that happens inside the promise takes a long time, there's still a chance that it could block the work the browser needs to do to render the page.
Promise Syntax
new Promise(function(resolve[, reject]) {
var value = doSomething();
if(thingWorked) {
resolve(value); // #1
} else if (somethingWentWrong) {
reject();
}
}).then(function(value) { // #2
// success!
return nextThing(value);
}).catch(rejectFunction);
// Note that "value" at #1 is the same with #2
Promise in ES2015:
new Promise((resolve, reject) => {
let value = doSomething();
if(thingWorked) {
resolve(value);
} else if (somethingWentWrong) {
reject();
}
}).then(value => nextThing(value))
.catch(rejectFunction);
Notation Promise
is a constructor. A promise can either be stored as a variable var promise = new Promise();
or simply work on it as soon as create it like the code block above.
Note that resolve
and reject
have the same syntax. resolve
leads to the next then
in the chain and reject
leads to the next catch
.
Incidentally, if there is a JavaScript error somewhere in the body of the promise, .catch
will also automatically get called.
An example of utilizing Promise to load image:
new Promise(function(resolve, reject) {
var img = document.createElement('img');
img.src = 'image.jpg';
img.onload = resolve;
img.onerror = reject;
document.body.appendChild(img);
})
.then(finishLoading)
.catch(showAlternateImage);
What's this
in Promise?
This question is very tricky: this
in Promise is different based on the JavaScript stansard you are using.
ES5
var someObj = function(){};
someObj.prototype.PromiseMethod = function(ms) {
return new Promise(function(resolve) {
console.log(this); // 1
setTimeout(function() {resolve();}, ms);
});
}
var instance = new someObj();
instance.PromiseMethod(3000).then(function(){console.log("done")});
The this
in Promise
written by ES5 is the global Object, or say the window.
ES2015 (ES6)
var someObj = function(){};
someObj.prototype.PromiseMethod = function(ms) {
return new Promise(resolve => {
console.log(this); // 2
setTimeout(function() {resolve();}, ms);
});
}
var instance = new someObj();
instance.PromiseMethod(3000).then(function(){console.log("done")});
The this
here in Promise
written by ES6 is the object someObj
, because ES6's this
will save the context in asynchronous operations. And this works with callback
, too.
Note that this is not special for Promise
, it's a special definition in ES6.
Fetch API
Fecth API uses native promises to simplify xml http requests. Here are two utility functions for fetch get and post:
function fetchGet(url, params) {
return fetch(url).then((res) => {
if (!res.ok) {
throw Error(res.statusText);
}
return res.json();
});
}
function fetchPost(url, params) {
console.log(JSON.stringify(params));
return fetch(url, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json());
}
Error Handling
Remember that .catch
is just shorthand for .then(undefined, rejectFunc)
:
// the following two blocks are equal
get('example.json')
.then(resolveFunc)
.catch(rejectFunc);
get('example.json')
.then(resolveFunc)
.then(undefined, rejectFunc);
The full function signature for then is actually this:
get('example.json')
.then(resolveFunc, rejectFunc); // resolveFunc and rejectFunc cannot be called both.
But note that in the full function signature, resolveFun
and rejectFunc
cannot be called both. Yet in the former signature of seperating then
and catch
, they can be called both.
In all cases, as soon as a promise rejects, then the JavaScript engine skips to the next reject function in the chain, whether that's in a .cath
or a .then
. For example:
get('example.json') // #A
.then(data => {
updateView(data); // #B
return get(data.anotherUrl);
})
.catch(error => { // 1
console.log(error);
return recoverFromError();
})
.then(doSomethingAsync)
.then(data => soSomethingElseAsync(data.property),
error => { // 2
console.log(error);
tellUserSomethingWentWrong();
})
An error happening at #A or #B will both be caught by 1 and 2 (2 is also rejectFunc as said before).
Here is another more complex error handling example, fill what numbers will be logged if errors occur on lines #A, #B, #C, and #D, and only these lines?
var urls = [];
async('example.json') // #A ----------> [?]
.then(function(data) {
urls = data.urls; // #B ----------> [?]
return async(urls[0]);
})
.then(undefined, function(e) {
console.log(1);
return recovery();
})
.catch(function(e) {
console.log(2);
return recovery(); // #C ----------> [?]
})
.then(function() {
console.log(3);
return async(urls[1]); // #D ------> [?]
})
.then(async, function(e) {
console.log(4);
ahhhIGiveUp();
});
Answer:
- Error at #A —> 1, 3: the first rejectFunc will catch the error and log 1, then recovery and continue to execute the next then, which logs 3;
- Error at #B —> 1, 3: the same situation with #A;
- Error at #C —> none: this is an interesting one, because the recovery function is in a rejectFunc, it is only going to be called only if another error happened before. But we ruled only one error can happen, so only #C error is an impossible case.
- Error at #D —> 4: the next reject function will get called.
Chaining
Asynchronous work may not be isolated, the next Promise may ask for the value from previous Promise to get executed properly, this is called chaining here.
There are two main startegies for performing multiple asynchronous actions: actions in series and actions in parallel.
- Actions in series: occur one after another;
- Actions in parallel: occur simultaneously.
This is an example contains both actions in series and actions in parallel:
getJSON('thumbsUrl.json')
.then(function(response) {
response.results.forEach(function(url) {
getJSON(url).then(createThumb);
});
});
In this example, 'thumbsUrl.json' is got first, then looping the url list to get thumbnails in parallel. One issue is that the thumbnails will be created in a random order. The timeline looks like this:
To keep the thumbnails in original order, we can wrap the getJSON
in the chain .then
so that the next getJSON
will not be executed until the thum has been created:
getJSON('thumbsUrl.json')
.then(function(response) {
var sequence = Promise.resolve();
response.results.forEach(function(url) {
sequence = sequence.then(function() {
return getJSON(url);
})
.then(createThumb);
});
});
The good news is: all thumbnails are in order. The bad news is: the cost time is much longer as shown below:
So how can we keep the order as well as chain actions in parallel instead of series? We can combine .map
method which output an array with Promise.all
which takes in an array of promises, executes them, and returns an array of values in the same order:
getJSON('thumbsUrl.json')
.then(function(response) {
var arrayOfPromises = response.results.map(function(url) {
getJSON(url);
});
return Promise.all(arrayOfPromises);
})
.then(function(arrayOfPlaneData) {
arrayOfPlaneData.forEach(function(planet) {
createPlanetThum(planet);
});
})
.catch(function(error) {
console.log(error);
});
Note that Promise.all
fails quick. Once a certain element in the array fails, the process will be interrupted. The timeline of this block of code is: