分享个人学习Promise时的心得和笔记,想通过这种方式让自己在巩固知识的时候印象更加深刻,同时Promise更加扎实一些,如果有说的不正确的地方也请大佬们帮忙指正一下,十分感谢!
(通过一些碎片化时间进行巩固和分享,持续更新和改进ing……)
Promise是ES6中异步编程的新的解决方案。
Promise是一个构造函数,可以用来封装一个异步操作并可以获取其成功 / 失败的结果值。
在Promise之前,所有的异步操作都是单纯的使用回调函数的方式来解决的
例如:
setTimeout(() =>{}, 2000) // 计时器,它就是使用的回调函数的方式来处理的
还有node.js中fs模块的readFile,以及Ajax请求,都是使用回调函数的方式进行处理。
一 . 为什么要使用Promise?
1 . Promise支持链式调用,可以解决回调地狱的问题。
什么是回调地狱?就是回调函数嵌套调用,例如:
setTimeout(() =>{
setTimeout(() =>{
setTimeout(() =>{
}, 2000)
}, 2000)
}, 2000)
看到这,对于拥有开发经验的人来说已经能够明白了,但是当我还是一个初学者的时候,确实有些难以理解,所以我想讲的更加通俗易懂一些。
// 这里有一个延迟1000毫秒后执行的计时器,我们可以将它当作一个网络请求
// 当我们向后端服务器发起请求的时候,直至服务器返回数据,中间这个过程,需要1000毫秒的时间
setTimeout(() =>{
// 在服务器响应并且返回了数据后,我们就要在这个回调函数中,做一些我们想要做的逻辑处理
// ......
}, 1000)
// 假设,当我们拿到这个结果之后,我需要使用这个响应结果作为参数,继续向服务器发起请求
// 所以紧接着,我需要在这个回调函数中,继续发送一个网络请求,这里还是用计时器来进行模拟
setTimeout(() =>{
setTimeout(() => {
// 在2000毫秒后,我们得到了第二个网络请求的响应结果,然后在回调函数中继续做逻辑处理
// ......
}, 2000)
}, 1000)
// 以此类推,我们需要发送许多个网络请求
// 但是请求时传递给后端的参数需要从前一个网络请求中获取
// 这样代码层级就开始不断向后缩进,形成了回调地狱
回调地狱的缺点也非常明显,代码可读性差,且不便于错误的处理。
什么是链式调用?我个人的理解如下:
const p = new Promise((resolve, reject) => {
resolve()
})
p
.then(() =>{})
.then(() =>{})
.then(() =>{})
.then(() =>{})
// 要理解Promise的链式调用,可以将代码做如下的改造
// 首先,第一个.then()执行结束后,返回了一个新的Promise
// 将这个Promise的保存给变量result,那么result现在就是一个新的Promise
// 然后调用result的.then()方法,继续执行下去
let result = p.then(()=> console.log('执行第一个then'))
let result1 = result.then(()=> console.log('执行第二个then'))
let result2 = result1.then(() => console.log('执行第三个then'))
// 也就是说,每一个.then()方法,都会返回一个全新的Promise,
// 简化下来也就形成了这种链式调用的结构
p
.then(() =>{})
.then(() =>{})
.then(() =>{})
.then(() =>{})
所以链式调用的方式更加灵活而且非常便于阅读。
二 . 创建一个Promise实例。
// 首先在创建Promise对象的时候,首先要传入一个函数,这个函数有两个参数
// 这两个参数都是函数类型
// 统一我们都定义为resolve 和 reject
// 例如:
const p = new Promise((resolve, reject) => {})
// 在这个Promise对象中,我们可以放入任何的异步操作
// 比如上面那个demo中通过计时器来获取了随机数
const p1 = new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
if (number >= 50) {
resolve()
} else {
reject()
}
}, 2000)
})
// 在Promise包裹的异步操作中,我们可以做自己想要的逻辑处理
// 在处理结束后,一定要给Promise一个结果,也就是说
// 如果成功了,要调用resolve()
// 如果失败了,要调用reject()
// 这样我们才能通过.then()方法对Promise进行后续的操作
// 例如下面的代码:
const p2 = new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
if (number >= 50) {
console.log('赢了')
} else {
console.log('输了')
}
}, 2000)
})
p2.then(() =>{
console.log('成功了')
}, () => {
console.log('失败了')
})
// 我在做完逻辑处理以后并没有给出Promise的结果
// 所以这段代码的运行结束后只会打印'赢了'或者'输了'
// 它不会进入到.then()中去,因为Promise没有返回结果
// .then()中接收两个函数,第一个函数处理Promise解析后的逻辑,第二个则处理拒绝的
// 上面的代码中,Promise既没有调用resolve(),也没有调用reject()
// 所以.then()中的代码就不会运行。
当我学习到这个地方的时候我就遇到了第一个问题,例如:在vue项目中,我想要封装一个Promise。
// util.js
const getResult = new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
console.log(number)
if (number >= 50) {
resolve()
} else {
reject()
}
}, 2000)
})
export {
getResult
}
// index.vue
// 所以在组件中,我直接引入了getResult
import { getResult } from '../utils/util.js'
// 我想要在某个click事件中,使用getResult.then()来获取随机数并且做后续的逻辑处理
// 结果问题来了,在index.vue挂载后,我发现控制台打印了一个number
// 显然它来自Promise中console.log(number)这段代码
// 我才发现,通过new Promise创建的Promise对象会立即执行其传入的函数
// 即使你还没有使用.then()方法
// 所以,util.js中getResult应该修改成如下的方式:
export function getResult() {
return new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
console.log(number)
if (number >= 50) {
resolve()
} else {
reject()
}
}, 2000)
})
}
// 暴露出去的应该是一个函数,而不是一个Promise,当调用这个函数的时候
// 它再返回一个Promise,这样就可以在特定的时间调用getResult获得结果了
三 . 在then()中获取Promise的处理结果。
const p = new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
if (number >= 50) {
resolve(number)
} else {
reject(number)
}
}, 2000)
})
p
.then((value) =>{
console.log('第一个then获取的value是', value)
return value
}, (reason) => {
console.log('第一个then获取的reason是', reason)
throw reason
})
.then((value) =>{
console.log('第二个then获取的value是', value)
}, (reason) => {
console.log('第二个then获取的reason是', reason)
})
在这个例子中:
- 当
p
被创建时,它会在2秒后随机解析或拒绝(其实官方的讲法是解析或者拒绝,我个人觉得以成功或者失败来说也没啥问题,只要能理解这个意思就行,Promise中的函数执行后,必定要给出一个结果,成功或者失败,成功了叫解析,失败了叫拒绝)。 - 如果
number
大于或等于50,p
会解析,并且第一个.then()
的第一个回调函数会被执行。这个回调函数会打印value
并返回它。由于返回了一个值,这个值会成为下一个.then()
中第一个回调函数的参数。 - 如果
number
小于50,p
会拒绝,并且第一个.then()
的第二个回调函数会被执行。这个回调函数会打印reason
并使用throw
语句抛出一个错误。这个错误会传递给下一个.then()
的第二个回调函数(这里有一个问题,如果在此处使用return reason,那么在下一个.then()中,则会执行第一个回调函数,原因在最后
)。 - 第二个
.then()
根据第一个.then()
的行为来决定执行哪个回调函数。如果第一个.then()
返回了一个值,那么第二个.then()
的第一个回调函数会被执行。如果第一个.then()
抛出了一个错误,那么第二个.then()
的第二个回调函数会被执行。
Promise链中的每个.then()
都会基于前一个.then()
的行为来创建并决定其状态。如果.then()
的回调函数返回了一个值,那么这个值会传递给下一个.then()
的第一个回调函数;如果.then()
的回调函数抛出了一个错误,那么这个错误会传递给下一个.then()
的第二个回调函数。
四 . Promise的状态。
在之前的Promise例子中,当通过new Promise创建了一个Promise对象或者说Promise实例后,在运行Promise中的函数时,都会调用resolve()或者reject()返回一个结果,其实 resolve() 和 reject() 就是在修改这个Promise的状态。在Promise的规范中定义了,Promise有三种状态,分别是:pending(进行中)、fulfilled(已完成)、rejected(已拒绝)。其实我也看了很多教程和文章,也有的人说只有两种,分别是fulfilled(已完成)、rejected(已拒绝),其实Promise只有这两种最终状态的说法才对,而pending状态也是Promise的一个重要组成部分,它表示异步操作还未完成,既不是成功也不是失败。如果将pending状态忽略,只考虑fulfilled和rejected,也是不完整的。
pending(进行中):初始状态,既不是成功,也不是失败状态。
fulfilled(已完成):意味着操作成功完成。
rejected(已拒绝):意味着操作失败。
一旦Promise对象的状态改变,就不会再变,也就是说这是一个不可逆的操作。而且在任何时候都可以得到这个结果。这也是Promise的一个重要特点。这使得我们可以依赖Promise的结果,而不用担心它的状态会在后续的操作中发生改变。
举个例子看一下Promise的状态的变化:
const p = new Promise((resolve, reject) => {
setTimeout(() =>{
let number = Math.ceil(Math.random() * 100)
if (number >= 50) {
resolve(number)
} else {
reject(number)
}
}, 2000)
})
console.log('p', p)
setTimeout(() => {
console.log('p', p)
}, 3000)
按照执行顺序来看,首先通过new Promise声明了一个Promise后,Promise中的函数立即开始执行,所以初始化的时候,Promise的状态是pending,2000毫秒后,得到的number是59,经过判断,执行了resolve(),那么Promise的状态被修改为了fulfilled,如果得到的number小于50,那么执行reject(),状态就会变成rejected,Promise的状态其实就是Promise实例对象的一个属性,叫作[PromiseState]。
五 . Promise对象的值。
也就是Promise实例对象的另一个属性,叫作[PromiseResult],它存储了Promise对象成功或者失败后的结果。
通过 resolve() 和 reject() 可以设置这个值,也就是上面的例子中,我们成功后调用resolve(number) 或者 失败后调用reject(number),其实也就是在设置Promise对象的值,一旦赋值,在后续的.then()方法中,就可以通过value 或者 reason将这个值取出来进行相关的操作。
说到这里,就可以先适当的整理一下之前学习的点,做一个小小的总结。
Promise的流程:
1. 当通过new Promise()创建一个Promise对象时,Promise的状态为pending。
2.接着开始执行Promise中的异步操作。
3.如果异步操作成功,调用resolve(),如果异步操作失败,则调用reject()。
4.调用resolve()后,Promise的状态会改为fulfilled,调用reject()后,Promise的状态会改为rejected,注意,Promise的状态,只能从pending变成fulfilled,或者从pending变成rejected,一旦Promise的状态变成fulfilled或者rejected,它的状态将不会再发生变化。
5.在调用.then()方法时,需要传递两个函数,然后根据Promise的状态来决定执行哪一个函数,如果Promise的状态为fulfilled,执行.then()中的第一个函数,如果Promise的状态为rejected,则执行.then()中的第二个函数。
六 . Promise的使用。
const p = new Promise((resolve, reject) =>{
console.log(1)
})
// 在创建一个Promise对象的时候,需要传入一个函数,这个函数叫作执行器函数。
// 也就是上面代码中 (resolve, reject) =>{ console.log(1) } 这一部分。
console.log(2)
// 通过控制台打印,结果如下:
// 1
// 2
由此可见,执行器函数会在Promise对象创建时立刻调用,而且是同步的。
异步操作会在执行器函数中去执行。
七 . Promise.resolve、Promise.reject 、 Promise.all 和 Promise.race。
注意:稍微开始有一点点绕了,但是一定要捋清楚这个概念。
首先,Promise是一个构造函数,用于创建 Promise 实例,当我们创建Promise实例的时候,传入一个执行器函数,在这个执行器函数中,需要传入resolve 和 reject 两个参数,用于修改Promise的状态。
当前要说的 Promise.resolve、Promise.reject 和 Promise.all,是属于Promise构造函数的静态方法。
所以两个resolve 和 reject千万不能混淆了。
先说Promise.resolve,这个方法返回一个 Promise 对象,这个对象的状态由传入的参数决定。
情况1:
const p1 = Promise.resolve('1')
console.log(p1)
const p2 = Promise.resolve(1)
console.log(p2)
const p3 = Promise.resolve(true)
console.log(p3)
const p4 = Promise.resolve({a: 1, b: 2, c: 3})
console.log(p4)
p4.then(value => {
console.log('p4 result', value)
})
const p5 = Promise.resolve([1, 2, 3])
console.log(p5)
p5.then(value => {
console.log('p5 result', value)
})
当传入一个非Promise对象的时候,Promise.resolve会返回一个新的Promise对象,并且状态为fulfilled,可以通过.then()获取到Promise的值进行操作。
情况2:
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
const p2 = new Promise((resolve, reject) => {
reject(1)
})
const p11 = Promise.resolve(p1)
const p22 = Promise.resolve(p2)
当传入一个Promise对象的时候,Promise.resolve将不做任何修改,直接返回这个Promise对象。
同时可以看出来,不管我传入的Promise对象的状态是fulfilled还是rejected,Promise.resolve都是原封不动的返回了。
再说Promise.reject,其实跟Promise.resolve的用法相同,当传入一个非Promise对象时,返回一个新的Promise对象,并且状态为rejected,可以通过.catch()获取到Promise的值进行操作。
不同的点在于:
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
const p2 = new Promise((resolve, reject) => {
reject(1)
})
const p11 = Promise.reject(p1)
const p22 = Promise.reject(p2)
console.log('p11', p11)
console.log('p22', p22)
神奇的一幕出现了,为什么传入一个Promise对象后,使用Promise.reject创建出来的Promise,它的值居然还是一个Promise,而不是我传入的resolve 或者 reject 的值,这跟Promise.resolve就有了很大的差别,并且如果想要获取到resolve 或者 reject的值,还需要做如下的操作:
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
const p2 = new Promise((resolve, reject) => {
reject(1)
})
const p11 = Promise.reject(p1)
const p22 = Promise.reject(p2)
console.log('p11', p11)
console.log('p22', p22)
p11.catch(reason => {
console.log(reason)
reason.then(value => {
console.log('p11 value', value)
})
})
p22.catch(reason => {
console.log(reason)
reason.catch(error => {
console.log('p12 error', error)
})
})
只有这样,我才能获取到原始的Promise对象中 resolve 或者 reject 传递的值,于是我迷糊了,查资料去:
这里,Promise.reject
创建了两个新的 Promise,p11
和 p22
,并且这两个 Promise 都被拒绝。但是,拒绝的原因分别是 p1
和 p2
,而不是我使用resolve 或者 reject传递出来的 1
。这意味着 p11
和 p22
的拒绝原因实际上是 Promise 实例,而不是简单的值。
现在,当我调用 .catch
方法来处理这些拒绝的 Promise 时,我得到了拒绝的原因(即原始的 p1
和 p2
Promise 实例),这就是为什么我可以在 .catch
方法的回调中调用 .then
或 .catch。
这段代码存在这样的情况是因为我将一个 Promise 实例作为另一个 Promise 的拒绝原因。在 .catch
方法中,我得到了这个拒绝原因(即原始的 Promise 实例),并且可以在它上面调用 .then
或 .catch
方法,这取决于这个原始 Promise 的状态。这通常不是推荐的做法,因为它可能会导致混淆和难以理解的代码逻辑。如果只是想创建一个新的、状态与原始 Promise 相同 的 Promise,应该使用 Promise.resolve(p)
而不是 Promise.reject(p)
。
Promise.resolve 和 Promise.reject 小结:
如果想要得到一个状态为失败的 Promise,可以直接使用 Promise.reject(reason)
,并传入任何您想要作为拒绝原因的值。这个值会作为新 Promise 的拒绝原因,而不会改变其类型。
然而,如果想要基于另一个 Promise 的状态来创建一个新的 Promise,并且希望新 Promise 的状态和值与原始 Promise 相同,那么应该使用 Promise.resolve(promise)
。Promise.resolve
会保持原始 Promise 的状态,无论是 fulfilled
还是 rejected
。如果传入的参数本身就是一个 Promise 实例,Promise.resolve
会直接返回这个实例,而不会创建一个新的 Promise。
再说Promise.all,接收一个参数,参数为Promise组成的数组,返回一个新的Promise,这个新的Promise的状态由Promise数组中每个Promise的状态来决定,如果在这个Promise数组中,所有的Promise执行结果都为成功,那么返回的Promise状态也为成功,并且返回的Promise的值为Promise数组中每个Promise的结果组成的数组,如果Promise数组中有一个Promise失败了,那么返回的Promise状态直接就失败了,并且这个返回的Promise的值就是第一个失败的Promise的结果。这个应该很好理解了,直接上代码:
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
const p2 = Promise.resolve(2)
const p3 = new Promise((resolve, reject) => {
resolve(3)
})
const result = Promise.all([p1, p2, p3])
result
.then(value => console.log('value', value))
.catch(reason => console.log('reason', reason))
// value [1, 2, 3]
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
const p2 = Promise.reject(2)
const p3 = new Promise((resolve, reject) => {
reject(3)
})
const result = Promise.all([p1, p2, p3])
result
.then(value => console.log('value', value))
.catch(reason => console.log('reason', reason))
// reason 2
最后是Promise.race,接收一个参数,参数也是Promise组成的数组,返回一个新的Promise,数组中第一个完成的Promise的状态和结果,就是新的Promise的状态和结果。这就是Promise数组中每个Promise运行速度的比拼了,谁先运行完成并且改变状态返回结果,新的Promise就继承谁的状态和结果。上代码:
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
const p2 = Promise.reject(2)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(3)
}, 500)
})
const result = Promise.race([p1, p2, p3])
result
.then(value => console.log('value', value))
.catch(reason => console.log('reason', reason))
// reason 2
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
// const p2 = Promise.reject(2)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(3)
}, 500)
})
const result = Promise.race([p1, p3])
result
.then(value => console.log('value', value))
.catch(reason => console.log('reason', reason))
// reason 3
八 . Promise状态的改变。
在之前其实已经学习过了,Promise在初始化的时候状态为 pending ,如果要修改状态,只需要在执行器函数中使用 resolve() 或者 reject() 即可,还有一种方式也可以修改Promise的初始化状态,就是使用throw关键字。
const p = new Promise((resolve, reject) => {
throw 'Error'
})
p.catch(reason => console.log('reason', reason))
console.log('p', p)
所以,修改Promise状态的方法有三种,
1. pending -> fulfilled, 使用resolve()。
2. pending -> rejected, 使用reject()。
3. pending -> rejected, 使用throw。
九 . Promise的异常穿透。
当使用Promise链在进行链式调用的时候,可以在最后指定失败时候的回调。运行时,前面任何一个节点失败了,都会传递到最后的失败回调中进行操作。
const p = new Promise((resolve, reject) => {
resolve('success')
})
p
.then(value => {
console.log('第一次调用then')
throw 'Error'
})
.then(value => {
console.log('第二次调用then')
})
.then(value => {
console.log('第三次调用then')
})
.catch(reason => {
console.log('reason', reason)
})
// 第一次调用then
// reason Error
十 . 中断Promise链。
const p = new Promise((resolve, reject) => {
resolve('success')
})
p
.then(value => {
console.log('第一次调用then')
})
.then(value => {
console.log('第二次调用then')
})
.then(value => {
console.log('第三次调用then')
})
.catch(reason => {
console.log('reason', reason)
})
上面的代码中,如果我在执行完第一个.then()后,不想执行后面的代码了,也就是说要中断这个Promise链,只有一种方式,那就是返回一个pending状态的Promise。
在第一个.then()方法中,加入return new Promise(() => {}),即可中断这个promise链。
原理很简单,如果在第一个.then()中返回一个任意的值,那么它会返回一个成功的Promise对象,如果返回一个失败的Promise或者throw抛出错误,它依旧返回一个失败的Promise,只有一个pending状态的值会阻断后续的.then()方法执行。
比如说,现在定义了一个新的Promise,但是执行器函数中并没有做任何操作,所以它的状态为pending,那么我在调用.then()方法后,它也不会执行,因为Promise的状态没有发生改变,.then()或者.catch()只能是在Promise成功或者失败后才会运行其中的回调函数。利用这一特性就可以中断Promise链。