一、 为什么我们需要Promise?
在Promise
出现之前,我们一般用定义回调函数来完成异步操作(这里补充一下,异步操作并不一定计算量大或者要等很长时间,只要我们不想为了等待某个异步操作而阻塞线程执行,在任何时候都可以使用)
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value * 2),1000);
double(3);
//6,大概1000毫秒后
大部分情况下,回调嵌套都不是很多,但是某些特殊情况下,比如说异步返回值又以来另一个异步返回值,那么回调的情况会愈加复杂,代码就会非常繁琐,嵌套回调的代码维护起来十分困难,这种情况我们称之为——“回调地狱”。于是乎,Promise
(期约)应运而生。
二、期约基础
Promise
,期约,是对尚不存在的结果的一个替身。可以理解为它保存着某个未来才会结束的事件的结果。Promise
提供统一的API,各种异步操作都可以用同样的方法进行处理。
ECMAScript新增了引用类型Promise
,所以Promise
是一个对象,我们可以通过new操作符
来实例化。
期约是一个有状态的对象。有以下三种状态:
- 待定(pending)
- 兑现(fullfilled,也称解决,resolved)
- 拒绝(reject)
待定(pending)是期约的最初始状态。在待定状态下,期约可以落定 (settled)为兑现状态(fulfilled)状态——代表成功,或拒绝状态(reject)——代表失败。
无论落定成哪种状态都是不可逆的。只要期约的状态由待定变为兑现/拒绝,期约的状态就不再改变。
非常重要的是,期约的状态是私有的,不可以直接通过JavaScript检测到,另外,期约的状态也不能被外部的JavaScript代码修改。期约故意将异步行为封装起来,从而隔离外部的同步代码。
每个期约只要状态切换为兑现,就会有一个私有的内部值(value),每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。
Promise
的构造函数接收一个参数 :执行器函数,并且,构造器函数需要传入两个参数
- resolve:异步操作执行成功后的回调函数,调用resolve()会把状态切换为fullfilled/resolved
- reject:异步操作执行失败后的回调函数,调用reject()会把状态切换为rejected,同时抛出错误
三、 Promise基本用法
光说不练假把式,让我们先来new
实例化一个Promise对象康康。
let p = new Promise((resolve, reject) => {
//做一些异步操作
setTimeout(() => {
console.log('执行完成');
resolve('YES!!');
}, 2000);
});
注意!这段代码里面我们只是把一个新的Promise
实例赋给了p,并没有调用它,但是这个Promise
里的异步函数会立刻执行。所有我们不需要它立即执行的时候,可以把它包在一个函数中,在需要的时候去运行这个函数。
1. Promise.resolve()
期约并不是一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。Promise.resolve()
是静态方法,可以通过调用此静态方法,实例化一个解决的期约。
可以给此静态方法传入一个参数,把任何值都可以转换为一个期约
let p1 = new Promise((resolve, reject) => resolve() );
let p2 = Promise.resolve()
setTimeout(console.log, 0, Promise.resolve() );
//Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3) );
//Promise<resolved>: 3
//会省略多余的参数
setTimeout(console.log, 0, Promise.resolve(4, 5, 6) );
//Promise<resolved>: 4
对于这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。所以可以说Promise.resolve()
是一个幂等方法。这个幂等性会保留传入期约的状态。
这个静态方法可以包装任何非期约值,包括错误对象,并把它转换为解决的期约。
let p = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p);
//Promise <resolved>: Error:foo
2. Promise.reject()
与Promise.resolve()
类似,Promise.reject()
会实例化一个拒绝的期约,抛出一个异步错误(这个错误不可以通过try/catch捕获,只能通过拒绝处理程序捕获)
let p1 = new Promise((resolve, reject) => reject() );
let p2 = Promise.reject()
//p1和p2实际上是一样的
但是,Promise.reject()并没有幂等性。如果给它传入一个期约对象,它会把这个期约当成它返回拒绝期约的理由
let p = new Promise(() => {} );
setTimeout(console.log, 0, p); //Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); //Promise <pending>
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
//Promise<rejected>: Promise <resolved>
同步/异步执行的二元性
try/catch
之所以不可以捕获Promise.reject()抛出的错误,是因为拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的。这也暴露出**Promise
期约的真正异步特性**:它们是同步对象,但也是异步执行模式的媒介。
四、期约的实例方法
期约的实例方法是链接外部同步代码与内部异步代码之间的桥梁。
Promise.prototype.then()
Promise.prototype.then()
是为期约实例添加处理程序的主要方法。then()
接收两个参数:onResolved
处理程序和onRejected
处理程序,分别在期约进入“兑现”和“拒绝”状态时执行。这俩操作一定是互斥的,因为期约状态只能转换为最终状态一次。
两个处理程序参数都是可选的,并且传给then()
的任何非函数类型的参数都会被静默忽略。如果想只提供onRejected
参数,那就要在onResolved
参数上传入undefined
,避免在内存中创建多余的对象。
Promise.prototype.then()
方法返回一个新的期约实例。这个新期约实例基于onResolved
处理程序的返回值构建,也就是说,返回值会通过Promise.resolve()
包装来生成新期约,onRejected
处理程序的返回值也会被Promise.resolve()
包装,因为onRejected
处理程序的任务就是捕获异步错误,只要它成功地捕获了这个错误,就完成了它的使命,所以应该返回一个解决期约。
function onReasolved(id) {
setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise( (resolved, reject) => {
setTimeout(resolve, 3000));
let p2 = new Promise( (resolved, reject) => {
setTimeout(reject, 3000));
p2.then(null, () => onRejected('p2'));
//p2 rejected (3秒后)
Promise.prototype.catch()
Promise.prototype.catch()
方法用于给期约添加拒绝处理程序,它只接收一个参数:onRejected
处理程序,它其实就是一个语法糖,相当于调用Promise.prototype.then(null, onRejected)
。它返回的也是一个新的期约实例。
let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
//下面两种方式是一样的~
p.then(null, onRejected); //rejected
p.catch(onRejected); //rejected
Promise.prototype.finally()
Promise.prototype.finally()
方法用于给期约添加onFinally
处理程序,不管期约转换为解决或者是拒绝,它都会执行,可以帮助onResolved
和onRejected
处理程序出现冗余代码,但是它也没办法知道期约的状态时解决还是拒绝,所以一般我们用它来添加清理代码。。它返回一个新的期约实例。
期约连锁
上面每个期约实例的方法(then()、catch()、finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法,所以我们可以把期约逐个串联起来,形成连缀方法,构成所谓的“期约连锁”。
let p = new Promise((resolve, reject) => {
console.log('first');
resolve();
});
p.then( () => console.log('second'))
.then( () => console.log('third'));
.then( () => console.log('fourth'));
//first
//second
//third
//fourth
Promise.all()和Promise.race()
Promise类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()
和Promise.race()
Promise.all()
PromisePromise.all()
静态方法创建的期约,会在一组期约全都解决之后,才会解决,它返回一个新期约。要是有一个期约在待定,合成的期约也待定,要是有一个期约是拒绝,合成的期约也会拒绝。
let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})
let p = Promise.all([Promise1, Promise2, Promise3])
p.then(funciton(){
// 三个都成功则成功
}, function(){
// 只要有失败,则失败
})
const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2 延时一秒')
}, 1000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p3 延时两秒')
}, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('p5 rejected 延时1.5秒')
}, 1500)
})
// 所有Promise实例都成功
Promise.all([p1, p2, p3])
.then(res => {
console.log(res)
})
.catch(err => console.log(err)) // 2秒后打印 [ 'p1', 'p2 延时一秒', 'p3 延时两秒' ]
// 一个Promise实例失败
Promise.all([p1, p2, p4])
.then(res => {
console.log(res)
})
.catch(err => console.log(err)) // p4 rejected
// 一个延时失败的Promise
Promise.all([p1, p2, p5])
.then(res => {
console.log(res)
})
.catch(err => console.log(err)) // 1.5秒后打印 p5 rejected
// 两个Promise实例失败
Promise.all([p1, p4, p5])
.then(res => {
console.log(res)
})
.catch(err => console.log(err)) // p4 rejected
Promise.race()
Promise.race()
静态方法创建的期约,是一组集合中最先解决或拒绝的期约的镜像。race,竞赛,谁先完成,谁先落定期约状态,我Promise.race()
就会包装其解决值或拒绝理由,并返回一个新的包装期约。
//请求某个图片资源
function requestImg(){
var p = new Promise((resolve, reject) => {
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = '图片的路径';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise.race([requestImg(), timeout()]).then((data) =>{
console.log(data);
}).catch((err) => {
console.log(err);
});
const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2 延时一秒')
}, 1000)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p3 延时两秒')
}, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('p5 rejected 延时1秒')
}, 1500)
})
// p1无延时,p2延时1s,p3延时2s
Promise.race([p1, p2, p3])
.then(res => console.log(res))
.catch(err => console.log(err)) // p1
// p4无延时reject
Promise.race([p4, p2, p3])
.then(res => console.log(res))
.catch(err => console.log(err)) // p4 rejected
// p5 延时1.5秒reject,p2延时1s
Promise.race([p5, p2, p3])
.then(res => console.log(res))
.catch(err => console.log(err)) // 1s后打印: p2 延时一秒
完结撒花~~~~