1. Promise 的两个特点
(1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这个也是 Promise 这个名字的由来,代表只要“承诺”了便无法改变。
(2)一旦状态确定,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态只有两种情况,从 pending 到 fulfilled 和从 pending 到 rejected。只要这两种情况发生了,状态就凝固了,会一直保持这个结果,这时就成为 resolved(已定型)。如果已定型,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是如果你错过了它再去监听,是没有结果的。
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得异步操作更加容易。
但是,Promise 对象也有一定的缺点:首先,无法取消 promise,一旦建立它就会立即执行,无法中途取消。其次,如果不设置回调函数,promise 内部抛出的错误不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪个阶段(是刚刚开始还是即将完成)。
若某些事件反复地发生,一般来说,使用 stream 模式是比部署 Promise 更好的选择。
2. 基本语法
ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例
const promise = new Promise(function(resolve,reject){//函数里的两个参数由 JavaScript 提供,不用自己部署
//some code
if(/*异步操作成功*/){
resolve(value);
}else{
reject(error);
}
});
Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数
then 中的两个参数,第二个可选,都接受 promise 对象传出的值作为参数
promise.then(function(value){
//success
},function(error){
//failure
});
下面是一个简单的 Promise 对象的例子
function timeout(ms){
return new Promise((resolve,reject) => {
setTimeout(resolve,ms,'done');
});
}
timeoue(100).then((value) =>{
console.log(value);
});
Promise 一旦建立就会立即执行,then 方法指定的回调函数,在当前脚本所有同步任务执行完后才会执行。
下面是加载图片的例子
function loadImageAsync(url){
return new Promise(function(resolve,reject){
const image = new Image();
image.onload = function(){
resolve(image);
};
image.onerror = function(){
reject(new Error("could not load image at"+url));
};
image.src = url;
});
}
resolve 函数和 reject 函数的参数会传递给回调函数,它们的参数也可以是一个 Promise 对象,即一个异步操作的结果是返回另一个异步操作。
const p1 = new Promise(function(resolve,reject){
setTimeout(() => reject(new Error('fail')),3000);
});
const p2 = new Promise(function(resolve,reject){
setTimeout(() => resolve(p1),1000);
});
p2
.then(resolve => console.log(resolve))
.catch(error => console.log(error))
//Error:fail
/*由于 p2 返回的是另一个 Promise 的状态,导致 p2 自己的状态无效了,由 p1 的状态控制 p2 的状态,
所以后面的 then 语句和 catch 语句都变成针对 p1 了。执行时,p1 3 秒之后变为 rejected ,p2 的状态
在 1 秒后改变,返回 p1 。又过了 2 秒后, p1 变为 rejected,导致触发 catch 方法指定的回调函数。*/
若 Promise 对象中,resolve 方法(reject 同)后面还有语句,则后面的语句会先执行,这是因为 resolve 总会晚于本轮循环的同步任务。所以我们最好在 resolve 前面加上 return 语句,这样就不会有意外。
//例1
const p = new Promise(function(resolve,reject){
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
//2
//1
//例2
const p = new Promise(function(resolve,reject){
return resolve(1);
console.log(2); //不会执行
});
3. Promise.prototype.then():then 方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调参数。一般来说,不要在 then() 方法里面定义 Rejected 状态的回调函数(then 的第二个参数),总是使用 catch 方法比较好。
4. Promise.prototype.catch():这个方法是 .then(null,rejection) 或 .then(undefined,rejection) 的别名,用于指定发生错误时的回调函数。Promise 对象的错误具有 “冒泡” 性质,会一直向后传递,直到被捕获为止。
getJson('/posts.json').then(function(posts){
//...
}).catch(function(error){
//处理 getJson 和前一个回调函数运行时发生的错误
console.log("发生错误! ",error);
});
reject()的作用,等同于抛出错误。
const promise = new Promise(function(resolve,reject){
throw new Error('test');
});
promise.catch(function(error){
console.log(error);
});
//Error:test
//等同于
const promise = new Promise(function(resolve,reject){
try{
throw new Error('test');
}catch(e){
reject(e);
}
});
promise.catch(function(error){
console.log(error);
});
//等同于
const promise = new Promise(function(resolve,reject){
reject(new Error('test'));
});
promise.catch(function(error){
console.log(error);
});
跟传统 try/catch 代码块不同的是,若没有使用 catch()指定错误处理的回调函数,Promise 对象发生的错误不会传递到外层代码,即不会做出任何反应。
const someAsyncThing = function(){
return new Promise(function(resolve,reject){
//下面一行信息报错,因为 x 没有声明
resolve(x + 2);
});
};
someAsyncThing().then(() => {console.log('everything is great')});
setTimeout(() => {console.log(123)},2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
以上代码说明,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“ Promise 会吃掉错误”。这个脚本放在服务器执行,退出码就是 0 (即表示执行成功)。
不过,Node.js 有一个专门监听未捕获的 reject 错误的事件:unhandleRejection,可以在监听函数里面抛出错误。第一个参数是错误对象,第二个参数是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
process.on('unhandleRejection',function(error,p){
throw error;
});
注意,Node 有计划在未来废除 unhandleRejection 事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码部位 0。
5. Promise.prototype.finally():用于指定不管 Promise 对象状态最后如何,都会执行的操作。finally() 不接受任何参数。
以下一个例子是服务器使用 Promise 处理请求,最后使用 finally 方法关掉服务器。
server.listen(port)
.then(function(){
//...
})
.finally(server.stop);
6. Promise.all():用于将多个 Promise 实例,包装成一个 Promise 实例。其参数可以不是数组,但必须是 Iterator 接口,且返回的每个成员都是 Promise 实例。
const p = Promise.all([p1,p2,p3]);
其中 p1,p2,p3 都是 Promise 实例,若不是实例,就会先调用 Promise.resolve(),将参数转成 Promise 实例。
p 的状态由 p1,p2,p3 决定
(1)只有当 p1,p2,p3 的状态都为 fulfilled , p 的状态才会为 fulfilled,此时 p1,p2,p3 的返回值组成一个数组,传递给 p 的回调函数
(2)只要 p1,p2,p3 中有一个状态为 rejected,p 的状态就会为 rejected,此时第一个被 rejected 的实例的返回值,会传递给 p 的回调函数。
注意,若作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected ,就不会出发 Promise.all() 的 catch 方法。
const p1 = new Promise(function(resolve,reject){
resolve("hello");
})
.then(result => result)
.catch(e => e);
const p2 = new Promise(function(resolve,reject){
throw new Error("报错了");
})
.then(result => result)
.catch(e => e);
Promis.all([p1,p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//["hello",Error:报错了]
//p1 的状态为 resolved,p2 实例执行完 catch 后,状态也会变成 resolved,故 Promise.all() 的状态也是 resolved ,因此会调用 then()指定的回调函数。
若 p2 没有自己的 catch 方法,就会调用 Promise.all() 的 catch 方法。
const p1 = new Promise(function(resolve,reject){
resolve("hello");
})
.then(result => result)
.catch(e => e);
const p2 = new Promise(function(resolve,reject){
throw new Error("报错了");
})
.then(result => result);
Promis.all([p1,p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//Error:报错了
7. Promise.race():同样是将多个 Promise 实例包装成一个 Promise 实例。
Promise.race() 和 Promise.all() 一样,如果参数不是 Promise 实例,会先调用 Promise.resolve()将参数转为 Promise 实例再进一步处理。
下面是一个例子,若在指定时间内没有获得结果,就将 Promise 的状态变为 rejected,否则变为 resolved。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function(resolve,reject){
setTimeout(() => reject(new Error('request timeout')),3000);
})
]);
p
.then(console.log)
.catch(console.error);
//若在 3 秒之内,fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 指定的方法回调函数
8. Promise.allSettled():接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些实例都返回结果,无论是 fulfilled 还是 rejected,包装实例才会结束。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
下面是返回值用法的例子。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
//过滤出成功的请求
const succesefulPromise = results.filter(p => p.status === "fulfilled");
//过滤出失败的请求,请输出原因
const error = results
.filter(p => p.status === "rejected")
.map(p => p.reason);
//每个对象都有 status 属性,当 Promise 实例的状态为 fulfilled 时,status 有 value 属性,Promise 状态为 rejected 时,status 有 reason 属性。
有时候,我们不关心异步操作的结果,只关心异步操作有没有结束。这时,Promise.allSettled() 就很有用,Promise.all() 无法做到这一点。
const urls = [/*...*/];
const requests = urls.map(x => fetch(x));
try{
await Promise.all(requests);
console.log("所有请求都成功");
}catch{
console.log("至少一个请求失败,其他请求可能还没结束");
}
9. Promise.any():接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个是 fulfilled 状态,那么包装实例就会变成 fulfilled 状态,参数实例全部都为 rejected 状态时,包装实例才会为 rejected 状态。
Promise.any() 与 Promise.race() 很相似,只有一点不同,就是不会因为某个 Promise 状态为 rejected 而结束。
Promise.any() 抛出的错误不是一个一般的错误,而是一个 AggregateError 实例,它相当于一个数组,每个成员对应一个被 rejected 的操作所抛出的错误。
new AggregateError() extends Array -> AggregateError
const err = new AggregateError();
err.push(new Error('first error'));
err.push(new Error('second error'));
throw err;
下面是一个例子
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var rejected2 = Promise.reject(Infinity);
Promise.any([resolved,rejected,rejected2]).then(function(result){
console.log(result); //42
});
Promise.any([rejected,rejected2]).catch(function(result){
console.log(result); //[-1 Infinity]
});
10. Promise.resolve():将现有对象转为 Promise 实例。
Promise.resolve('foo');
//等同于
new Promise(resolve => resolve('foo'));
Promise.resolve() 的参数分成 4 中情况。
(1)若参数是 Promise 实例,那么 Promise.resolve() 将不做任何修改,原封不动的返回这个实例。
(2)参数是一个 thenable 对象(即具有 then()的对象),将其转为 Promise 实例之后,立即执行 thenable 对象的 then 方法
let thenable = {
then: function(resolve,reject){
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value){
console.log(value); //42
});
(3)参数不是具有 then 方法的对象,或者根本不是对象,则 Promise.resolve() 返回一个新的对象,状态为 resolved。
const p = Promise.resolve('hello');
p.then(function(s){
console.log(s); //hello
});
(4)不带任何参数,直接返回一个 resolved 状态的 Promise 对象。若希望得到一个 Promise 对象,最简单的方法就是用 Promise.resolve()
需要注意的是,立即 resolve()的 Promise 对象,是在本轮事件循环(event loop)的结束时执行,而不是在下一轮事件循环的开始时。
setTimeout(function(){
console.log("three");
},0);
Promise.resolve().then(function(){
console.log('two');
});
console.log('one');
//one
//two
//three
11. Promise.reject():也会返回一个 Promise 实例,状态为 rejected。
注意,Promise.reject() 的参数,会原封不动的作为 reject 的理由,变成后续方法的参数,这一点与 Promise.resolve() 不同。
const thenable = {
then:function(resolve,reject){
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e =>{
console.log(e === thenable); //true
});
以上代码中 Promise.reject() 的参数是一个 thenable 对象,执行以后,后面 catch 的参数不是 reject()抛出的 “出错了”,而是 thenable 对象。
12. Promise.try():让同步函数同步执行,异步函数异步执行,并且有统一的 API。
第一种写法:
const f = () => console.log('now');
(async () => f())(); //立即执行函数
console.log('next');
//now
//next
以上代码,若 f 是同步的,就会得到同步的结果,若 f 是异步的,就可以用 then 指定下一步,如
(async () => f())()
.then(...)
需要注意的是,async() 会吃掉 f() 抛出的错误,如果想要捕获错误,要使用 promise.catch()
(async () => f())()
.then(...)
.catch(...)
第二种写法:使用 new Promise()
const f = () => console.log('now');
(
() => new Promise(resolve,reject){
resolve => resolve(f());
}
)();
cosole.log('next');
//now
//next
鉴于这是一个很常见的需求,所以有一个提案,用 Promise.try() 代替以上的写法
const f = () => console.log('now');
Promise.try(f);
console.log('next');
//now
//next