0 前言
我一直以为我对Promise比较了解,相关的方法已经非常熟悉了,直到我看到这篇文章,里面提出了这样一个问题:
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四种 promises 的区别是什么
//1
doSomething().then(function(){
return doSomethingElse();
}).then(finalHandler);
//2
doSomething().then(function(){
doSomethingElse();
}).then(finalHandler);
//3
doSomething().then(doSomethingElse())
.then(finalHandler);
//4
doSomething().then(doSomethingElse)
.then(finalHandler);
我当时看了是吃惊的,因为我想,这都什么玩意儿!!!所以我把Promise的方法复习了一遍,并且仔细读了上面提到的那篇文章,于是就有了这篇文章。
在前端开发的学习中,新工具层出不穷,理解当前的基础是要理解过去,然后了解将来。就异步调用而言,ES6中引入Promise简化异步操作,主要针对的问题就是回调函数的层层嵌套(金字塔问题),除了阅读不方便之外,只能在当前回调函数函数内部处理异常,这个很难做。Promise通过then和catch方法实现链式调用,每一次调用都返回一个Promise对象,摆脱了回调函数层层嵌套的问题和异步代码“非线性执行”的问题;另外,所有回调函数的报错都可以通过Promise统一处理,catch可以捕获先前调用中所有的异常(冒泡特性)。但是Promise仅仅是对回调做了简化处理,ES7中的async函数更厉害,结合Promise,完全不用回调函数,以近似同步的写法实现异步操作,所需要的仅仅是一个async和await关键字而已。本文仅介绍Promise对象,以及ES6中Promise对象具有的一些操作方法。
1 简单Promise对象
ES6中原生实现了Promise对象,通过状态传递异步操作的消息。Promise对象有三种状态:pending(进行中)、resoleved(fulfilled,已完成)、rejected(已失败),根据异步操作的结果决定处于何种状态。一旦状态改变就不可再变,状态改变仅有两种pending=>rejected、pending=>resolved。
优点:避免了层层嵌套的回调函数,并提供了统一的接口,使异步操作更加容易。
缺点:无法取消Promise;如果不设置回调函数,内部错误无法反映到外部。
1.1 创建Promise实例
Promise构造函数接收两个参数:resolve和reject,这是两个由JavaScript引擎自动提供的函数,不用自己部署。resolve函数在异步操作成功时调用,作用是将Promise对象的状态由pending变为resolved,并将异步操作的结果传递出去。reject函数在异步操作失败时调用,作用是将Promise对象的状态由pending变为reject,将异步操作报错传递出去。
then方法可以接受两个回调函数作为参数,第一个是Pormise对象的状态变为resolved时调用,另一个是当Promise对象的状态变为rejected时调用,这两个回调函数都接受Promise对象实例创建过程中resolve函数和reject函数传出的值作为参数。第二个参数可选,事实上一般通过Promise.prototype.catch()调用发生错误时的回调函数,通过then调用异步操作成功时的回调函数。
实例1:
//返回Promise对象,setTimeout中传递的resolve参数为’done’
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
}); //done
实例2:Promise执行流
//创建Promise实例
let promise = new Promise(function(resolve, reject) {
console.log('Promise');//立即执行
resolve();
});
//resolved状态调用在当前脚本所有同步任务执行完才会执行
promise.then(function() {
console.log('Resolved.');
});
//立即执行
console.log('Hi!');
对以上代码,Promise新建后立即执行,所以首先输出的是“Promise”。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。
reject函数在异步操作失败时调用,因此参数常常是一个错误对象(Error Object);resolve函数在操作成功时调用,参数常常是正常值或者另一个Promise实例,这表征了异步嵌套,异步嵌套的外层Promise要等待内层Promise的状态决定下一步状态。
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2.then(result => console.log(result)) //p1 is rejected, p2 is the same as p1
.catch(error => console.log(error)) // Error: fail
由于p2的resolve方法将p1作为参数,p1的状态决定了p2的状态,如果p1的状态是pending,p2的回调函数会等待p1的状态改变;如果p1的状态是resolved或rejected,p2的回调函数立即执行。p2的状态在1秒之后改变,resolve方法返回的是p1。此时,由于p2返回的是另一个Promise,所以后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。
1.2 Promise.prototype.then()
then方法为Promise实例添加状态改变时的回调函数,返回一个新的Promise实例,可以采用链式写法,前一个then方法的返回值作为后一个then方法的参数:
getJSON("/posts.json").then(function(json) { // json comes from “/posts.json”
return json.post;
}).then(function(post) { //post comes from json.post
// ...
});
如果第一个then方法内的回调函数返回一个Promise对象,后续的then方法会根据这个新的Promise对象的状态执行回调函数。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("Resolved: ", comments);
}, function funcB(err){
console.log("Rejected: ", err);
});
第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB。回调函数一般是匿名函数,上述仅仅是为了便于理解写成命名函数。
1.3 Promise.prototype.catch()
catch(rejection)方法是then(null,rejection)的别称,仅仅当发生错误时执行,catch的存在是将错误回调函数从then()方法中剥离出来。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。要是后续then方法里面报错,就与前面的catch无关了。如果最后一个catch方法内部抛出错误,是无法捕获的。为了避免潜在错误,最好是在最后用一个catch方法兜底。
1.4 Promise.all():偏重状态改变的逻辑与关系
用于将多个Promise实例包装成一个新的Promise实例。如果内部参数不是Promise实例,就调用Promise.resolve()将参数转换为Promise实例。
var p = Promise.all([p1, p2, p3]);//接受一个Promise数组作为参数
p的状态由p1、p2、p3决定,呈现&关系,fulfilled对应1,rejected对应0,分成两种情况:
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值(rejected的顺序有没有类似与操作的顺序?),会传递给p的回调函数。
const databasePromise = connectDatabase();
const booksPromise = databasePromise
.then(findAllBooks);
const userPromise = databasePromise
.then(getCurrentUser);
Promise.all([
booksPromise,
userPromise
])
.then(([books, user]) => pickTopRecommentations(books, user));
上面代码中,booksPromise和userPromise是两个并行执行的异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommentations这个回调函数。
1.5 Promise.race():偏重状态改变的时间顺序
Promise.race()和Promise.all()同样是将多个Promise实例包装成一个新的Promise实例,但是只要实例数组中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给新实例的回调函数。
var p = Promise.race([p1, p2, p3]);
Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
上面代码中,如果5秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
1.6 Promise.resolve()
将现有对象转化为Promise对象,根据参数不同有不同结果:
(1) 参数是一个Promise实例,Promise.resolve()将原对象返回;
(2) 参数是具有then方法的对象,Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出42。
(3) 如果参数是一个原始值(基本类型值),或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
(4) Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。
var p = Promise.resolve();
p.then(function () {
// ...
});
立即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时,这个很好理解,通过resolve产生的Promise对象然后调用then函数和先产生Promise对象,对象转换成resolved后再执行then函数是一样的,都是在本轮事件轮询的末尾执行。
//下一轮事件轮询开始
setTimeout(function () {
console.log('three');
}, 0);
//本轮事件轮询末尾
Promise.resolve().then(function () {
console.log('two');
});
//立即执行
console.log('one');
// one two three
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(’one‘)则是立即执行,因此最先输出。
1.7 Promise.reject()
Promise.reject()返回一个Promise实例,实例状态为rejected。Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
实例1:
var p = Promise.reject('出错了');
// 等同于var p = new Promise((resolve, reject) => reject('出错了'))
//参数就是’出错了’
p.then(null, function (s) {
console.log(s)
});// 出错了
实例2:
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
//参数就是thenable
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。
2 加深Promise理解
Promises 给予我们的就是在我们使用异步Callback时丢失的最重要的语言基石: return, throw 以及堆栈。但是想要 promises 能够提供这些便利给你的前提是你知道如何正确的使用它们。
2.1 使用Promise.resolve()创建Promise对象
任何有可能 throw 同步异常的代码都是一个后续会导致几乎无法调试异常的潜在因素。但是如果你将所有代码都使用Promise.resolve() 封装,那么你总是可以在之后使用 catch() 来捕获它。因此方法2要优于方法1。
方法1:
new Promise(function(resolve,reject){
resolve(someSynchronousValue);
}).then(/*-------------*/);
方法2:
function somePromiseAPI() {
return Promise.resolve().then(function(){
doSomethinThatMayThrow();
return ‘foo’;
}).then(/*------------*/);
}
2.2 catch() 与 then(null, ...) 根据使用场景并非完全等价
以下代码等价:
somePromise().catch(function (err)){
//handle error
});
//////////////////////////////////////
somePromise().then(null, function(err)) {
//handle error
}
但是以下代码不等价:
somePromise().then(function(){
return someOtherPromise();
}).catch(function(err){
//error
});
///////////////////////////////
somePromise().then(function(){
return someOtherPromise();
},function(err){
//error
});
因此,当你使用 then(resolveHandler, rejectHandler) 这种形式时,rejectHandler 并不会捕获由 resolveHandler 引发的异常。最好不使用then()的第二个参数,而是总是使用catch(),唯一例外是写一些异步的Mocha测试时,使用then()的第二个参数,希望抛出用例的异常。
2.3 promises vs promises factories
当我们希望执行一个个的执行一个 promises 序列,即类似 Promise.all() 但是并非并行的执行所有 promises。你可能天真的写下这样的代码:
function executeSequentially(promise){
var result = Promise.resolve();
promises.forEach(function (promise)){
result = result.then(promise);
});
return result;
}
不幸的是,这份代码不会按照你的期望去执行,你传入 executeSequentially() 的 promises 依然会并行执行。其根源在于你所希望的,实际上根本不是去执行一个 promises 序列。依照 promises 规范,一旦一个 promise 被创建,它就被执行了。因此你实际上需要的是一个 promise factories 数组。
我知道你在想什么:“这是哪个见鬼的 Java 程序猿,他为啥在说 factories?” 。实际上,一个 promises factory 是十分简单的,它仅仅是一个可以返回 promise 的函数:
function executeSequentially(promiseFactories){
var result = Promise.resolve();
promiseFactories,forEach(function (promiseFactory){
result = result.then(promiseFactory)
});
return result;
}
function promiseFactory(){
return somethingThatCreatesAPromise();
}
为何这样就可以了?这是因为一个 promise factory 在被执行之前并不会创建 promise。它就像一个 then 函数一样,而实际上,它们就是完全一样的东西。如果你查看上面的 executeSequentially() 函数,然后想象 myPromiseFactory 被包裹在 result.then(...) 之中,也许你脑中的小灯泡就会亮起。在此时此刻,对于 promise 你就算是悟道了。
2.4 promises 穿透
Promise.resolve(‘foo’).then(Promise.resolve(‘bar’)).then(function(result){
console.log(result);
});
执行结果并非是bar,而是foo,这是因为当then()接受非函数的参数时,会解释为then(null),这就导致前一个Promise的结果穿透到下面一个Promise。正确的写法是在then()方法内部包含函数:
Promise.resolve(‘foo’).then(function(){
return Promise.resolve(‘bar’);
}).then(function(result){
console.log(result);
});
2.5 Promise.all()
Promise.all()以一个Promise数组作为输入,返回一个新的Promise,特点在于它会并行执行数组中的每个Promise,并且每个Promise都返回后才返回结果数组,这就数组的异步版map/forEach方法。但是如果需要返回两个不相关的结果,使用Promise.all()可以产生两个不相关的数组结果;但是如果后一结果要依靠前一个结果产生,此时在Promise里使用嵌套也就可以的:
getUserByName(‘bill’).then(function(user){
return getUserAccountById(user.id);
}).then(function (userAccount){
/*-----------------*/
});
或者在内部使用嵌套:
getUserByName(‘bill’).then(function(user){
return getUserAccountById(user.id).then(function(userAccount){
/*--------------------*/
});
});
忘记使用catch:没人可以保证不出错,所以还是在最后加一个catch吧!
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四种 promises 的区别是什么
//1
doSomething().then(function(){
return doSomethingElse();
}).then(finalHandler);
// doSomething()返回一个Promise实例,但是后续的then方法里是一个匿名函数,该函数产生一个新的Promise实例并返回这个实例,因此finalHandler的参数就是这个实例的resolve返回值。
doSomething
/-----------------/
doSomethingElse(undefined)
/----------------------------------/
finalHandler(reulstOfDoSomethingElse)
/--------------------------------------------------/
//2
doSomething().then(function(){
doSomethingElse();
}).then(finalHandler);
//doSomething()返回一个Promise实例,但是后续的then方法里是一个匿名函数,该函数产生一个新的Promise实例,但是由于这个函数没有返回值,因此finalHandler函数没有参数。
doSomething
/-----------------/
doSomethingElse(undefined)
/----------------------------------/
finalHandler(undefined)
/----------------------------------/
//3
doSomething().then(doSomethingElse())
.then(finalHandler);
// doSomething()返回一个Promise实例,但是后续的doSomethingElse()是一个立即执行函数,不接受上一Promise实例的resolve参数,所以参数是undefined,这时doSomething()的Promise穿透到finalHandler,finalHandler的参数就是该Promise的resolve参数。
doSomething
/-----------------/
doSomethingElse(undefined)
/----------------------------------/
finalHandler(reulstOfDoSomething)
/--------------------------------------------------/
//4
doSomething().then(doSomethingElse)
.then(finalHandler);
//doSomething()返回一个Promise实例,随后调用doSomethingElse以上一实例的resolve参数产生第二个Promise,最后是finalHandler以上一实例的resolve参数产生第三个Promise。
doSomething
/-----------------/
doSomethingElse(resultOfDoSomething)
/----------------------------------/
finalHandler(reulstOfDoSomethingElse)
/--------------------------------------------------/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。