这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天
Promise
创建Promise
Promise存储值的方式非常的特别,我们先来看看它的构造函数:
const promise = new Promise(executor)
创建Promise时需要一个executor(执行器)为参数,执行器是一个回调函数,进一步调用它大概长这个样子:
const promise = new Promise((resolve, reject) => {})
回调函数在执行时会收到两个参数,两个参数都是函数。第一个函数通常命名为resolve,第二个函数通常会命名为reject。向Promise中存储值的关键就在于这两个函数,可以将想要存储到Promise中的值作为函数的参数传递,像是这样:
const promise = new Promise((resolve, reject) => {
resolve("哈哈")
})
这样我们就将”哈哈”这个字符串存储到了Promise中,那么问题又来了,为什么需要两个函数存储值呢?很简单,resolve用来存储运行正确时的数据,reject用来存储运行出错时的错误信息。我们在使用Promise时需要根据不同的情况,调用不同的函数来存储不同的数据。
Promise为什么整了如此复杂的一种方式来存储数据呢?如果仅仅是存储其他的数据,这么做确实有点脱了放。但是Promise是专门为了异步调用而生的,所以Promise中存储的主要是异步调用的数据,也就是那些本来需要通过回调函数来传递的数据。在Promise中,可以直接调用异步代码,在异步代码执行完毕后直接调用resolve或reject来将执行结果存储到Promise中,这就解决了异步代码无法设置返回值的问题。换句话说,异步代码的执行结果可以直接存储到Promise中,像是这样:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("哈哈")
}, 10000)
})
上例中通过setTimeout实现了一个异步调用,定时器会在10秒后执行,并调用resolve将”哈哈”存储到Promise中。现在你应该能够理解为什么Promise有这么一个奇怪的存储值的方式了吧?
获取Promise中的数据
Promise存储数据的方式奇特,读取方式同样特殊。我们需要通过Promise的实例方法来读取存储到Promise中的数据。现在我们有这样一个Promise,Promise中通过resolve存储了一个数据”哈哈”:
const promise = new Promise((resolve, reject)=>{
setTimeout(() => {
resolve("哈哈")
}, 10000)
})
Then
then是Promise的实例方法,通过该方法可以获取到Promise中存储的数据。它需要一个回调函数作为参数,Promise中存储的数据会作为回调函数的实参返回给我们:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("哈哈")
}, 10000)
})
promise.then((data) => {
console.log(data) // "哈哈"
})
注意:这种方式只适合读取通过resolve存储的数据,如果存储数据时出现了错误,或者是通过reject存储的数据,这种方式是读取不到的:
const promise = new Promise((resolve, reject) => {
throw new Error("出错了!")
setTimeout(() => {
resolve("哈哈")
}, 10000)
})
promise.then((data) => {
console.log(data)
})
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("哈哈")
}, 10000)
})
promise.then((data) => {
console.log(data)
})
上边的两块代码都是读取不到数据的,而且运行时会在控制台报出错误信息。这是因为,then的第一个参数只负责读取Promise中代码正常执行的结果,也就是只有Promise中数据正常时才会被调用。当Promise中的代码出错,或通过reject来添加数据时,我们还需要为其指定第二个参数来处理错误。
then的第二个参数同样是一个回调函数,两个回调的函数的结构相同,不同点在于第一个回调函数会在没有异常时被调用。而第二个函数会在出现错误(或通过reject存储数据)时调用。
const promise = new Promise((resolve, reject) => {
throw new Error("主动抛出错误")
setTimeout(() => {
resolve("哈哈")
}, 10000)
})
promise.then((data) => {
console.log(data)
}, (err) => {
console.log("出错了", err)
})
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject("哈哈")
}, 10000)
})
promise.then((data) => {
console.log(data)
}, (err) => {
console.log("出错了", err)
})
上边两个案例中,then的第二个回调函数会执行。执行时异常信息或时reject中返回的数据会作为参数传递。现实开发中,第二个回调函数通常会用来编写异常处理的代码。
原理
在Promise中维护着两个隐藏的值PromiseResult和PromiseState,PromiseResult是Promise中真正存储值的地方,在Promise中无论是通过resolve、reject还是报错时的异常信息都会存储到PromiseResult中。PromiseState用来表示Promise中值的状态,Promise一共有三种状态:pending、fulfilled、rejected。pending是Promise的初始化状态,此时Promise中没有任何值。fulfilled是Promise的完成状态,此时表示值已经正常存储到了Promise中(通过resolve)。rejected表示拒绝,此时表示值是通过reject存储的或是执行时出现了错误。
当我们调用Promise的then方法时,相当于为Promise设置了一个回调函数,换句话说,then中的回调函数不会立即执行,而是在Promise的PromiseState发生变化时才会执行。如果PromiseState从pending变成了fulfilled则then的第一个回调函数执行,且PromiseResult的值作为参数传递给回调函数。如果PromiseState从pending变成了rejected则then的第二个回调函数执行,且PromiseResult的值作为参数传递给回调函数。
then执行后每次总会返回一个新的Promise,并将then中回调函数的返回值存储到这个Promise中,如果没有指定返回值则新Promise中不会存储任何值。
const promise = new Promise((resolve, reject)=>{
resolve("第一步执行结果")
})
const promise2 = promise.then(result => {
console.log("收到结果:", result)
return "第二步执行结果" // 会作为新的结果存储到新Promise中
})
const promise3 = promise2.then(result => {
console.log("收到结果:", result)
return "第三步执行结果" // 会作为新的结果存储到新Promise中
})
在简化一些可以这样写:
const promise = new Promise((resolve, reject)=>{
resolve("第一步执行结果")
})
promise.then(result => {
console.log("收到结果:", result)
return "第二步执行结果" // 会作为新的结果存储到新Promise中
}).then(result => {
console.log("收到结果:", result)
return "第三步执行结果" // 会作为新的结果存储到新Promise中
})
第一个then用来读取上边我们创建的Promise中存储的结果,第二个then用来读取第一个then所返回的结果,依此类推我们就可以根据需要一直then下去,如此便解决了“回调地狱”的问题。
有了Promise后,在异步函数中我们便不再需要通过回调函数来返回结果,取而代之的是返回一个Promise,并将异步执行的结果存储到Promise中,像是这样:
function sum(a, b){
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(a + b)
}, 10000)
})
}
由于sum的返回值是一个Promise,所以我们不在需要通过回调函数来读取结果:
sum(123, 456).then(result => {
console.log("结果为:", result) // 结果为: 579
})
如果需要连续多次调用,也不会在有“回调地狱的问题”:
sum(123, 456)
.then(result => sum(result, 777))
.then(result => sum(result, 888))
.then(result => console.log(result))
Catch
除了then以外,Promise中还有一个catch方法,catch和then使用方式类似,但是catch中只需要一个回调函数作为参数。catch中回调函数的作用等同于then中的第二个回调函数,会在执行出错时被调用。既然有了then的第二个参数,为什么还需要一个catch呢?两个回调函数都写到then中,会导致代码不够清晰,但是多了一个catch后立刻就变的不一样了,开发时通常会在then中编写正常运行时的代码,catch中编写出现异常后要执行的代码:
const promise = new Promise((resolve, reject) => {
reject("出错了")
})
// 出现异常,then中只传了一个回调函数,无法读取数据
// promise.then((data) => {
// console.log(data)
// })
// 出现异常,可以通过catch来读取数据
promise.catch(err => {
console.log(err)
})
当Promise中代码执行出错时(或者reject执行时),如果我们调用的是catch来处理数据,则Promise会将错误信息传递给catch的回调函数,我们便可以在catch中处理异常,同时catch回调函数的返回值会作为下一步Promise中的数据向下传递。如果我们调用了then来处理数据,同时没有传递第二个参数,这时then是不会执行的,而是将错误信息直接添加到下一步返回的Promise中,由后续的方法处理。在后续调用中如果有catch或then的第二个参数,则正常处理。如果没有,则报错。
简言之,处理Promise时,如果没有对Promise中的异常进行处理(无论是then的二参数,还是catch),则异常信息总是会封装到下一步的Promise中进行传递,直到找到异常处理的代码位置,如果一直没有处理,则报错。
这种设计方式使得我们可以在任意的位置对Promise的异常进行处理,例如有如下代码:
function sum(a, b) {
return new Promise((resolve, reject) => {
if (Math.random() > 0.7) {
throw new Error("出错了")
}
resolve(a + b)
})
}
sum(123, 456)
.then(result => sum(result, 777))
.then(result => sum(result, 888))
.then(result => console.log(result))
上例代码中,sum函数有一定的几率会出现异常,但是我们并不确定何时会出现异常,这时有了catch就变的非常的方便,因为在出现异常后所有的then在异常处理前都不会执行,所以我们可以将catch写在调用链的最后,这样无论哪一步出现异常,我们都可以在最后统一处理。像是这样:
sum(123, 456)
.then(result => sum(result, 777))
.then(result => sum(result, 888))
.then(result => console.log(result))
.catch(err => console.log("哎呀出错了,随便返回一个吧", 8888))
当然如果我们想在中间处理异常也是没有问题的,只是需要注意在链式调用中间处理异常时,由于后续还有then要执行,所以一定不要忘了考虑是否需要在catch中返回一个结果供后续的Promise使用:
sum(123, 456)
.then(result => sum(result, 777))
.catch(err => {
// 也可以在调用链的中间处理异常
console.log("出错了,我选择忽略这个错误,重新计算")
return sum(123, 456)
})
.then(result => sum(result, 888))
.then(result => console.log(result))
.catch(err => console.log("哎呀出错了,随便返回一个吧", 8888))
还有一点要强调一下,在Promise正常执行的情况下如果遇到catch,catch是不会执行的,此时Promise中的结果会自动传递给下一个Promise供后续使用。
Finally
finally也是Promise的实例方法之一,和then、catch不同,无论何种情况finally中的回调函数总会执行,通常我们在finally中定义一些无论Promise正确执行与否都需要处理的工作。注意,finally的回调函数不会接收任何参数,同时finally的返回值也不会成为下一步的Promise中的结果。简单说,finally只是编写一些必须要执行的代码,不会对Promise产生任何实质的影响。
静态方法
Promise的静态方法直接通过Promise类去调用,这些方法可以帮助我们完成一些更加复杂的异步操作。
Promise.All
当我们有多个Promise需要执行,且需要多个Promise都执行完毕,在将他们的结果进行统一处理时,我们便可以使用Promise.all来帮助我们完成这项工作。
Promise.all(iterable)
all需要一个数组(可迭代对象)作为参数,数组中可以存放多个Promise。调用后,all方法会返回一个新Promise,这个Promise会在数组中所有的Promise都执行后完成,并返回所有Promise的结果。比如:
function sum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b)
}, 5000);
})
}
Promise.all([sum(1, 1), sum(2, 2), sum(3, 3)])
.then((result) => {
console.log(result)
})
上例中,调用三次sum,且将其添加到数组中传递给all。调用后all会返回一个新的Promise,当三次计算都完成后,新的Promise也会变为完成状态,并将三次执行的结果封装到数组中返回。
Promise.AllSettled
all仅有当全部Promise都完成时才会返回有效数据,而allSettled用法和all一致,但是它里边无论Promise是否完成都会返回数据,只是他会根据不同的状态返回不同的数据。
成功:{status:”fulfilled”, value:result}
失败:{status:”rejected”, reason:error}
Promise.Race
race会返回首先执行完的Promise,而忽略其他未执行完的Promise
Promise.Any
any会race类似,但是它只会返回第一个成功的Promise,如果所有的Promise都失败才会返回一个错误信息。
Promise.Resolve
Promise.resolve用来创建一个新的Promise实例,且直接通过resolve存入一个数据。
Promise.Reject
Promise.reject用来创建一个新的Promise实例,且直接通过reject存入一个数据。