参考
https://www.bilibili.com/video/BV1x84y1g7ns/中有详细的视频讲解,下面是我的学习笔记。
Promise 初步认识
首先大致了解一下promise到底长什么样子,先定义下面这样一个request,封装一个promise函数用ajax从本地的data.js中获取数据。
打印出来可以看到,除了catch,then等几个函数之外,这个promise主要包含一个state属性和一个result。其实这里的PromiseResult就是promise中的resolve和reject返回的内容。但是这里为什么是undefined而不是resolve中的xhr.responseText呢,是因为同步api先执行了,次数还没有执行resolve。
Promise的设计思路
想要深入理解promise,必须要了解promise的实现。promise设计的思想是基于状态的,异步操作成功或失败仅仅会改变一个状态值,后续的所有操作都是基于这个状态进行的。这与回调函数不同,回调函数中当操作成功或失败之后往往直接执行了callback。下面手写一个简单的promise。
promise有以下几个特点:
1.首先这个promise传入了一个函数包括两个参数resolve和reject。
2.promise实现了对异步操作的状态管理。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
3.一旦状态改变,就冻结了,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
4.then函数要能接收两个回调函数来执行resolve和reject之后的业务。同时能获得promise执行后的resolve结果或reject结果。
5.then要能实现链式调用。
class Mypromise{
constructor(executor) { // 构造函数,这里的executor就是构建promise时的匿名函数
executor(this.resolve,this.reject) //执行这个匿名函数,这个函数里面有两个回调函数,分别是resolve和reject。所以需要在下面定义这两个函数。
}
state = PENDING //promise的状态
result = undefined // resolve的结果存在这里
reason = undefined // reject的结果存在这里
successCB = undefined
failCB = undefined
resolve = (result)=>{
if(this.state !== PENDING) //一定要从PENDING -> FULLFILLED 如果已经FULLFILLED或者REJECTED 则不能再通过resolve和reject来改变状态了。
return
this.state = FULLFILLED
this.result = result // 如果需要传递resolve和reject的参数就要先存起来
this.successCB && this.successCB(this.result) //意思是如果successCB不为undefine则执行他,执行语句与then中的正常执行是相同的。只不过是从then阶段移到了这里。
}
reject = (reason) =>{
if(this.state !== PENDING)//一定要从PENDING -> FULLFILLED
return
this.state = REJECTED
this.reason = reason
this.failCB && this.failCB(this.reason)
}
then = (successCB,failCB) =>{ // then函数只要在状态确定了才会执行,并且分别在成功和失败两种情况下运行不同的回调函数。
// 通常来说,前面的resolve和reject在封装promise时,会在异步api中使用,也就是说他们是异步执行的,这使得then函数一般情况下比resolve和reject要先执行。所以要处理状况为pending的情况。
if(this.state === FULLFILLED)
successCB(this.result) // 我们需要回调函数能获得resolve和reject传递的参数,因此我们在then中的回掉函数中传入实参。这样then函数中的回调函数就可以访问到result或者是reason。
else if (this.state === REJECTED)
failCB(this.reason)
else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
{
this.successCB = successCB
this.failCB = failCB
}
}
}
// 下面是测试,发现已经可以实现基本的功能
let p = new Mypromise((resolve,reject) => {
setTimeout(()=>{ // 模拟一个异步请求的情况
resolve(123)
})
})
p.then((result)=>{
console.log(result)
})
上面的代码已经可以实现基本的promise功能,但是还有一些缺陷。比如下面的情况,就会发现只打印了一次123。结合一下上面的源代码,异步的情况下,肯定是then函数先执行。在这个例子中,会先执行两次then函数,此时的then函数状态仍然是pending,所以会把本该执行的回调函数console.log(result)放到前面的resolve中执行,相当于是this.successCB = successCB执行了两次。
但是因为这段代码只有一个Mypromise实例,也就是只执行了一个resolve,就算你多次使用then函数,在上面的异步情况下,也只会打印一次,也就是在resolve里的那一次。因此为了这种需求,需要更改then函数中的pending情况。
let p = new Mypromise((resolve,reject) => {
setTimeout(()=>{
resolve(123)
})
})
p.then((result)=>{
console.log(result)
})
p.then((result)=>{
console.log(result)
})
这种初始为undefine,然后在then函数中赋值函数的方法不够用了。所以我们用一个数组来存,每次在then函数中没能执行,推迟到resolve或reject阶段的函数都存起来,最后在resolve和reject阶段把数组里的函数一起执行。
class Mypromise{
constructor(executor) { // 构造函数,这里的executor就是构建promise时的匿名函数
executor(this.resolve,this.reject) //执行这个匿名函数,这个函数里面有两个回调函数,分别是resolve和reject。所以需要在下面定义这两个函数。
}
state = PENDING //promise的状态
result = undefined // resolve的结果存在这里
reason = undefined // reject的结果存在这里
successCB = []
failCB = []
resolve = (result)=>{
if(this.state !== PENDING) //一定要从PENDING -> FULLFILLED 如果已经FULLFILLED或者REJECTED 则不能再通过resolve和reject来改变状态了。
return
this.state = FULLFILLED
this.result = result // 如果需要传递resolve和reject的参数就要先存起来
while(this.successCB.length)
this.successCB.shift()(this.result) // 数组中存的是函数,取出一个然后把result作为参数传进去。
}
reject = (reason) =>{
if(this.state !== PENDING)//一定要从PENDING -> FULLFILLED
return
this.state = REJECTED
this.reason = reason
while(this.failCB.length)
this.failCB.shift()(this.reason) // 数组中存的是函数,取出一个然后把reason作为参数传进去。
}
then = (successCB,failCB) =>{ // then函数只要在状态确定了才会执行,并且分别在成功和失败两种情况下运行不同的回调函数。
// 通常来说,前面的resolve和reject在封装promise时,会在异步api中使用,也就是说他们是异步执行的,这使得then函数一般情况下比resolve和reject要先执行。所以要处理状况为pending的情况。
if(this.state === FULLFILLED)
successCB(this.result) // 我们需要回调函数能获得resolve和reject传递的参数,因此我们在then中的回掉函数中传入实参。这样then函数中的回调函数就可以访问到result或者是reason。
else if (this.state === REJECTED)
failCB(this.reason)
else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
{
successCB && this.successCB.push(successCB) // 判空,不为空就放进数组
failCB && this.failCB.push(failCB) // 防止一个promise的多个then调用,所以把任务放进数组中存起来,可以理解为一个队列,然后在resolve或reject阶段依次执行。
}
}
}
// 测试
let p = new Mypromise((resolve,reject) => {
setTimeout(()=>{
resolve(123)
})
})
p.then((result)=>{
console.log(result)
})
p.then((result)=>{
console.log(result)
})
到这一步,仍然有一个问题没解决,那就是then的链式调用。现在的需求是,需要链式调用then的同时能访问上一个promise的resolve和reject的结果。思路就是我们的每个then函数返回的是一个全新的promise,这个返回的全新的promise中的resolve和reject就是上一个promise中的resolve和reject,就像是新瓶装旧酒,新的promise只放的resolve和reject都是之前一样的。这样一来,每个then函数代表的都是一个新的promise,每个promise都有then函数,这样一个链式结构就连起来了。你就可以一直.then.then.then了,同时每个then都能保存上一个promise的结果。
为此,我们可以改写then函数如下(其他的部分不变)
then = (successCB,failCB) =>{
return new Mypromise((resolve,reject)=>{ // 思路是用新的瓶子(promise)来装之前promise的内容
if(this.state === FULLFILLED) //这里的this指的是旧promise的的state 不是新的promise
//successCB(this.result) 这里本来是直接执行成功的回调函数。现在我们整理一下需求:1.这个函数需要执行 2.执行的结果需要传递给新的promise。
resolve(successCB(this.result)) // 我们直接执行这个函数,然后巧妙的把这个successCB函数的结果通过resolve存在新的promise中的result里。这里可以看上面的resolve函数的实现。
else if (this.state === REJECTED)
reject(failCB(this.reason)) // 和上面一样,这个语句相当于执行failCB,同时把执行结果传给reject存到新的promise的reason里。
else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
{
//successCB && this.successCB.push(successCB) 这是之前的写法
successCB && this.successCB.push(()=>{
resolve(successCB(this.result))
}) // 同样我们要保证在异步情况下,新的promise中的then函数推迟到resolve和reject阶段再执行。
failCB && this.failCB.push(()=>{
reject(failCB(this.reason))
}) // 执行时我们需要把数组里所有successCB或者failCB的结果都用resolve或reject存到新promise的result和reason中。可能有点晕,反复琢磨一下就懂了。
}
})
}
then函数中最后的pending阶段代码可能有点难以理解,只要对比之前的写法就会轻松很多。原来是把要做的回调函数存到数组里推迟执行,现在是把新瓶装旧酒这个过程,变成了旧酒存到数组里,推迟到resolve或reject阶段再一次性装到新瓶子里。
我们可以整理一下思路,根据上面这个代码,我们的then函数返回的是一个新的promise。我们可以想象三个角色:
- 原来的promise -> promise1
- 原来的promise的then函数 -> promise1.then
- 新的promise -> promise2。
下面的例子是以promise中使用的异步api为前提的,也就是说then函数执行在resolve和reject前面。
promise1.then返回的就是promise2。 这个promise1.then创建了一个新的promise2.在promise1异步任务完成时,以成功为例,promise1的resovle会执行,他会通过promise2的resolve函数,将promise1的sucessCB的结果存在promise2的result中。也就是这个过程有一点点绕。然后我们得到的promise2状态一定和promise1相同(因为promise1执行resolve才会让promise2执行resolve,前者执行reject,后者才会执行reject),此外promise2的reason或result一定是promise的sucessCB或failCB的结果。
至此就实现了一个promise的主要特点。真正的Promise要复杂的多,还有一些其他功能比如race和all等等。