异步请求之Promise

130 阅读9分钟

Promise概念

  • 背景:异步任务的代码,不能保证按照顺序执行,如果我们非要代码顺序执行,就需要嵌套回调函数,像这种回调函数中嵌套回调函数的情况就叫做回调地狱;回调地狱会造成我们的代码可读性非常差,后期不好维护;而Promise的链式编程可以保证代码的执行顺序,更容易阅读和理解
  • 概念:Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理

异步/同步的概念

同步: 在执行某段代码时,在没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值,即可执行其他代码。也就是说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步。

异步: 当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情况称为异步。

Promise的状态、过程和特点

  • 三状态:
`pending`:   进行中,Promise创建时的状态
`fulfilled`: 已成功,在Promise执行器中调用resolve后的状态
`rejected`:  已失败,在Promise执行器中调用reject后的状态
  • 两过程

    pending-->fulfilled:resolved(已完成)

    pending-->rejected :rejected(已拒绝)

  • 两特点:

Promise对象的状态一经改变就不会再变,任何时候都可以获取结果:只要发生了状态的改变,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise对象的状态不受外界影响:Promise对象代表一个异步操作,只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

Promise用法

1.Promise的创建

let promise = new Promise((resolve,reject)=>{
    if(/* 异步操作成功 */) resolve(/*成功后向外传递的值*/);
    else reject(/*失败后向外给的提示信息*/);
})

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,resolve函数的作用是,将Promise对象的状态从“进行中”变为“成功”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“进行中”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

一般情况下,我们会用new Promise()来创建Promise对象。除此之外,也可以使用promise.resolve和 promise.reject这两个方法来创建。

2.常用方法

2.1 .then()

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

let promise = new Promise((resolve,reject)=>{
    resolve(100);
});
promise.then(data=>{
    console.log('成功,获取到值:'+data);//成功,获取到值:100
},error=>{

})

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

let promise = new Promise((resolve,reject)=>{
    resolve(100);//这里使用resolve抛出数值,则下一个then如有第一个回调参数,则执行第一个回调函数的代码块
});
promise.then(data=>{
    console.log('成功,获取到值:'+data);//成功,获取到值:100
    new Promise((resolve,reject)=>{
        reject(200);//这里使用reject抛出数值,则下一个then有第二个回调参数,则执行第二个回调函数的代码块
    })
},error=>{
    console.log('失败:'+error);
}).then(data=>{
    console.log('成功,获取到值:'+data);
},error=>{
    console.log('失败:'+error);//失败: 200
});

2.2 .catch()

catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

let promise = new Promise((resolve,reject)=>{
    reject(100);
});
promise.catch(error=>{
    console.log('发生错误:' + error);
})

catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中

2.3 .finally()

.finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

2.4 .all()

.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

let p1 =  new Promise((resolve,reject)=>{
    resolve('我是p1');
});
let p2 =  new Promise((resolve,reject)=>{
    reject('我是p2');
});
let p3 =  new Promise((resolve,reject)=>{
    resolve('我是p3');
});
let p = Promise.all([p1, p2, p3]);

上面代码中,all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。 p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

上面代码中p的状态就是第二种情况,此时第二个Promise实例的值被返回,如下图

image.png

我们再看一个例子:

let p1 = new Promise((resolve, reject) => {
    resolve('我是p1');
})
    .then(result => result)
    .catch(e => e);

let p2 = new Promise((resolve, reject) => {
    throw new Error('我是p2,我开始报错了');
})
    .then(result => result)
    .catch(e => e);

Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e));
//结果为:[我是p1,Error:我是p2,我开始报错了]

上面代码中,p1resolvedp2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

如果p2没有自己的catch方法,就会调用Promise.all()catch方法

2.5 .any()

ES2021 引入了any()。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回

let p = Promise.all([p1, p2, p3]);

上面代码中p状态的结果与使用all()方法相反但又有点不同p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成rejectedp的状态才会变成rejected不是一个数组

(2)只要p1p2p3之中有一个被fulfilledp的状态就变成fulfilled,此时第一个被resolve的实例的返回值,会传递给p的回调函数。

2.6 .race()

.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

let p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

.race()方法的参数与.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

2.7 .allSettled()

有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作,这个时候就需要使用allSettled()方法啦。

let p = Promise.allSettled([p1, p2,p3])
p.then(data=>{
    console.log(data);
});

上面代码中p的状态只可能变成fulfilled。它的回调函数接收到的参数是一个数组。该数组的每个成员都是一个对象,对应传入.allSettled()的数组里面的 Promise 对象,如下图。

image.png

成员对象的status属性的值只可能是字符串fulfilled或字符串rejected,用来区分异步操作是成功还是失败。如果是成功(fulfilled),对象会有value属性,如果是失败(rejected),会有reason属性,对应两种状态时前面异步操作的返回值。

2.8 .resolve()

有时需要将现有对象转为 Promise 对象,.resolve()方法就起到这个作用。

Promise.resolve()等价于下面的写法。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

.resolve()方法的参数分成四种情况。

  1. 参数是一个 Promise 实例 如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
  2. 参数是一个thenable对象 thenable对象指的是具有then方法的对象,比如下面代码中的这个对象。
let thenable = {
    a:1,
    then: function (resolve,reject) {
        resolve('我是thenable对象');
    }
};
let p = Promise.resolve(thenable);
p.then(data=>{
    console.log(data);//我是thenable对象
});

resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。上面代码中,thenable对象的then()方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then()方法指定的回调函数,输出'我是thenable对象'。

  1. 参数不是具有then()方法的对象,或根本就不是对象

resolve()方法返回一个新的 Promise 对象,状态为resolved:

let obj = {
    a:1,
};
let p = Promise.resolve(obj);
p.then(data=>{
    console.log(data);//{a:1}
});

上面代码生成一个新的 Promise 对象的实例p。由于对象没有then方法,返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve()方法的参数,会同时传给回调函数,输出:{a:1}

  1. 不带有任何参数

调用时不带参数,则直接返回一个resolved状态的 Promise 对象。

2.9 .reject()

reject方法和resolve不同,reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。

Promise练习

Promise.resolve(100).then(result => {
        console.log(`成功:${result}`);//100 
        return result / 10;
    },
    reason => {
        console.log(`失败:${reason}`);
        return Promise.resolve(200);
    }).then(result => {
    console.log(`成功:${result}`);//10 
    return Promise.reject(300);
}, reason => {
    console.log(`失败:${reason}`);
    return reason / 20;
}).then(result => {
    console.log(`成功:${result}`);
    return 0;
}, reason => {
    console.log(`失败:${reason}`);//300 
    return 1;
}).then(result => {
    console.log(`成功:${result}`);//成功1 
}, reason => {
    console.log(`失败:${reason}`);
});