一篇文章带你了解Promise

166 阅读10分钟

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

1. 为什么新增Promise

在介绍Promise之前,我们来聊一下,为什么ES6要新增Promise这个引用类型?

在ES6以前,我们要执行一个异步函数,通常是通过定义回调函数来监听异步函数执行完成,并获得执行的结果,如果我们要执行多个异步函数,并且前一个异步函数的结果与后一个异步函数有关联,那么我们需要深度嵌套回调函数(俗称“回调地狱”),来看一下代码演示

function operator(value){
    setTimeout(()=>{
        value=value*2
        setTimeout(()=>{
            console.log(value*3)
        },1000)
    },1000)
}
operator(3)//18 约2s后输出

上面的代码演示中,利用setTimeout函数演示异步函数的执行,第一个setTimeout函数1秒后执行回调函数,对value数据进行翻倍,然后再注册了一个setTimeout函数,1秒以后再调用回调函数进行数值操作,最后得到输出结果:18。上面的代码只是一个简单的演示,如果是一些复杂的应用场景,需要多次调用接口,那么嵌套的层级可能会更深,并且这样层层嵌套的函数没有办法做到统一的错误监听,需要在每一个执行函数中用try/catch的包裹来处理错误,这样的代码不利于维护,也不够直观,因此ES6新增了Promise类型,使得异步函数的逻辑可以优雅的组织起来,摆脱地狱回调

2. Promise原理

Promise对象代表未来即将要发生的事情,用来传递异步操作的最终结果

我们来认识一下Promise的一些有关定义

  1. Primise的三个状态

    pending-待定,最初态,在这个状态下可以落定为代表成功的兑现或者代表失败的拒绝

fulfilled-兑现,最终态,在这个状态下为不可逆

rejected-拒绝,最终态,在这个状态下为不可逆

  1. value,代表异步函数操作完成的值,当Promise状态改变为兑现之后可以访问这个值

  2. reason,代表异步函数操作失败的原因,当Promise被拒绝之后提供的拒绝理由

  3. Promise通过构造函数的两个函数参数来改变Promise的状态,其中调用resolve会把状态切换为兑现,而reject会把状态切换为拒绝,只要Promise状态发生了改变,后面再修改状态都是会不起作用的。

    const p =new Promise((resolve,reject)=>{
      resolve('success')
      reject('fair')//默认会失败
    })
    console.log(p)//Promise {<fulfilled>: 'success'}
    

3. Promise的实例方法

3.1 then

Promise实例会返回一个then方法,来访问最终的结果,同时接收两个参数onFulfilled, onRejected,这两个参数应该为函数类型,如果不是,则会被忽略,我们一般使用then方法来访问执行成功之后的结果。在调用then方法之后会返回一个新的Promise实例,因此then方法可以被链式调用

const t1=(value)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve(value+2)
        },1000)
    })
}
new Promise((resolve,reject)=>{
    resolve(1)
})
.then(result => {
    console.log(result)//1
    return t1(result)
})
.then(newResult => {
    console.log(newResult)//3  大约1秒后
    return t1(newResult)
})
.then(finalResult => {
    console.log(finalResult)//5  大约2秒后
})

上述例子演示了Promise的链式调用,当异步函数需要依次分步进行的时候,可以在then方法中调用,并且可以访问上一次执行的结果,这样的链式调用可以解决地狱回调的问题。但是这样的方法显然不是最佳实践,因为代码都写在一个地方,显得冗余,不方便维护,一般来说,一个独立完整的功能,我们需要单独拎出来,方便后续可以重复调用,我们可以改造一下上面的代码

const t1=(value)=>{
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve(value+2)
        },1000)
    })
}

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(t1, t1);
transformData(1).then((res)=>{
    console.log(res)//5
})

我们可以封装一个函数composeAsync,实现把上一次执行的结果传给下一个异步调用函数的功能,然后把需要依次执行的异步函数独立出来,统一放到一个数组中,使用composeAsync函数去实现调用,这样的话,就可以实现相同的逻辑可以重复利用,并且代码逻辑也比较清晰。

传递到 then() 中的函数会被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 的同步任务执行完成之后,才开始执行,可以猜一下下面的执行顺序

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(7)).then(() => console.log(2));
console.log(5);
//5->7->2->4

首先第一行声明了一个wait函数。

在第二行调用了这个函数初始化了一个Promise实例,而这个Promise初始化过程中创建了一个setTimeOut函数,这是一个宏任务,所以把它放在宏任务队列中。

然后到第三行,Promise.resolve()是同步任务,创建了一个Promise实例,然后调用了then方法,then方法是微任务,所以把它放在了微任务队列中,然后又调用then方法,因此把第二个微任务也放到队列中。

然后来到了第四行,执行同步任务打印输出5,同步任务执行完了,再执行微任务,微任务队列中有第三行放进去的两个then方法调用,因此依次执行打印输出7、2。

微任务执行完毕之后,再执行宏任务,setTimeout函数执行回调,调用resolve改变Promise状态,然后调用then方法,因为同步任务已经执行完成,因此直接执行then里面的方法,打印输出4

3.2 catch

catch方法其实是then(null,onRejected)方法的一个缩写,用来处理Promise执行失败之后的情况。catch同样也会返回一个新的Promise,可以有后续链式操作

new Promise((resolve, reject) => {
    console.log('初始化');
    resolve();
})
.then(() => {
    throw new Error('有哪里不对了');
    console.log('执行「这个」”');//并不执行
},(err)=>{
    console.log(err)
})
.then(() => {
    console.log('执行「中间这个」”');//并不执行
})
.catch((err) => {
    console.log(err);//Error: 有哪里不对了
})
.then(() => {
    console.log('执行「这个」,无论前面发生了什么');//执行「这个」,无论前面发生了什么
});

从上面的例子我们可以看到,在then方法中抛出的错误,同一级别的onRejeted并不能监听到,而是要顺着链式调用,被最近的catch捕获,中间的then函数的onResolve监听是被直接忽略了,而catch后面的then函数调用还是会继续执行,说明catch函数调用也是会返回一个新的Promise实例。

3.3 finally

finally() 方法返回一个Promise。在promise结束时,无论结果是兑现还是拒绝状态,都会执行指定的回调函数。这为Promise在不管有没有执行成功都需要执行的代码提供了一个语义化的调用方式。避免了同样的语句需要在then()catch()中各写一次的情况。

const p= new Promise((resolve,reject)=>{
    resolve(1)
})
.then(result => {
    console.log(result)//1
   throw new Error('2');
})
.finally(()=>{
  console.log('finally')//finally
})
setTimeout(console.log,0,p)//Promise {<rejected> Error: 2 at.....}

finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况,finally会返回一个新的Promise,这个新的Promise的状态以及值都是根据前一个Promise的状态以及值而定,上述例子中,then函数调用抛出了错误,返回的新的Promise状态是被拒绝的,因此finally返回的Promise状态也是被拒绝

4. Promise静态方法

4.1 resolve

可以通过Promise.resolve()方法创建一个已兑现的Promise,这个静态方法可以把任何值转换为Promise,如果传入的值也是一个Promise,那么Promise.resolve()会返回这个传入的值。

const t=Promise.resolve('11')
console.log(t)//Promise {<fulfilled>: '11'}   Promise.resolve把字符串转换为Promise对象
const p=new Promise(()=>{})
console.log(p===Promise.resolve(p))//true  Promise.resolve把Promise对象原封不动的返回

4.2 reject

Promise.resolve()方法相似,Promise.reject()会实例化一个被拒绝的Promise,但有一点不同,如果传入的值是一个Promise,那么这个Promise会成为拒绝的理由

const t=Promise.resolve('11')
console.log(Promise.reject(t))//Promise {<rejected>: Promise}  Promise.reject被拒绝的理由为一个Promise

4.3 all

Promise.all方法接收一个可迭代对象参数,并且只返回一个Promise实例,它是输入的所有Promiseresolve回调的结果。这个Promise.allresolve回调是在所有输入的Promiseresolve回调都结束,或者输入的iterable里没有Promise了的时候调用。它的reject回调是,只要任何一个输入的Promisereject回调执行或者输入不合法的Promise就会立即抛出错误,并且reject的是第一个抛出的错误信息。ES6的iterable类型有ArrayMapSetString

const t1=new Promise(()=>{
    console.log('t1')//t1
})
const p = Promise.all([1,2,3]);
const p2 = Promise.all([1,2,3, Promise.resolve(444)]);
const p3 =  Promise.all([1,Promise.reject(555)],t1,Promise.reject(66)]);
const p4 =  Promise.all('111');
const p5=Promise.all([])
setTimeout(function(){
    console.log(p);//Promise { [ 1, 2, 3 ] }
    console.log(p2);//Promise { [ 1, 2, 3, 444 ] }
    console.log(p3);//Promise { <rejected> 555 }
    console.log(p4);//Promise { [ '1', '1', '1' ] }
    console.log(p5);//Promise { [] }
},2000);

从上述的例子中可以看到,当Promise.all执行成功时,都会返回一个数组,数组里面的每一个值是对应项执行成功的结果,如果传入的可迭代对象中某一项不是Promise,他会直接返回该值,如果是Promise,那它会执行这个Promise

如果这个可迭代对象的某一项出现执行失败了,那么Promise.all也会变成执行失败的状态,Promise.all的reason值是对应的值,但是后面的项依旧会继续执行,只是状态不会再改变,reason也不会再改变

4.4 allSettled

Promise.allSettled方法返回一个在所有给定的Promise都已经兑现或拒绝后的Promise,并带有一个对象数组,每个对象表示对应的Promise结果。

当你有多个彼此不依赖的异步任务成功完成时,或者想知道每个promise的结果时,通常会使用它。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];
Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result)));
//{ status: 'fulfilled', value: 3 }
//{ status: 'rejected', reason: 'foo' }

Promise.allSettled方法是不管任务执行成功或者失败都会返回,在返回的对象数组中,会给出对应的状态以及结果。

4.5 any

Promise.any接收一个可迭代对象,只要其中的一个 Promise 成功,就返回那个已经成功的 Promise 。如果可迭代对象中没有一个 Promise 成功(即所有的 Promises 都被拒绝),就返回一个失败的PromiseAggregateError类型的实例,它是 Error 的一个子类,用于把对应的错误的原因收集在一起

const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});
const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});
const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);//很快完成
})
const promises = [
    Promise.reject('ERROR A'),
    Promise.reject('ERROR B'),
    Promise.reject('ERROR C'),
  ]
  
  Promise.any(promises).then((value) => {
    console.log('value:', value)
  }).catch((err) => {
    console.log('err:', err)//err: AggregateError: All promises were rejected
    console.log(err.message)//All promises were rejected
    console.log(err.name)//AggregateError
    console.log(err.errors)//['ERROR A', 'ERROR B', 'ERROR C']
  })

需要注意一点的是node.js 15.0.0 才支持Promise.any方法,因此低版本使用会提示报错

4.6 race

Promise.race方法返回一个 Promise,一旦迭代器中的某个Promise兑现或拒绝,返回的 Promise就会是兑现或拒绝。如果迭代包含一个或多个非Promise值或已解决或者已拒绝的Promise,那么Promise.race将解析为迭代中找到的第一个值。

const p1='111';
const p2 = new Promise((resolve, reject) => {
    console.log('p2')//p2
    setTimeout(resolve, 100, 'two');
});
const p3 = new Promise(function(resolve, reject) {
    console.log('p3')//p3
    setTimeout(resolve, 500, "three");
});
const p4 = new Promise(function(resolve, reject) {
    console.log('p4')//p4
    setTimeout(reject, 200, "four");
});

Promise.race([p1,p2,p3, p4]).then(function(value) {
  console.log(value); //111
}, function(reason) {
    console.log(reason)//没有触发
});

从上述例子可以看到,Promise.race会返回已解决的第一个值,但是其他的项还是会依次执行。