最近要开始校招了,对于准备面试题我觉得必不可少的就是如何自己实现一个promise,废话不多说,直接开始吧。
MyPromise的基本架构
这里提一嘴,由于是自己实现一个Promise,如果对Promise不是很熟练,建议先去熟悉Promise,磨刀不误砍柴工嘛
首先,我们可以通过new Promise来实例化一个Promise对象,并且这个实例身上具有then方法,catch方法,我们也可以通过Promise.resolve()来返回一个已解决的Promise对象,同样的还有Promise.reject,以及Promise.all(),Promise.race()等,所以我们根据这个来把架子先搭起来:
class MyPromise{
constructor(){
}
then(){}
catch(){}
static resolve(){}
static reject(){}
static all(){}
static race(){}
}
MyPromise 的三种状态改变
Promise具有三种状态,pending(等待),fulfilled(成功),rejected(失败),并且状态一旦从pending转换为其他状态,就无法再被改变了。所以我们先定义三个状态:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
constructor(){
this.status = PENDING//初始值默认为PENDING
}
then(){}
catch(){}
static resolve(){}
static reject(){}
static all(){}
static race(){}
}
为了接下来大家不会混乱,所以我们先用Promise把基本语法写出来,然后再一一映射:
Promise:
const p1 = new Promise((resolve,reject)=>{
if(网络请求成功){
resolve(网络请求回来的数据)
}else{
reject(失败原因)
}
})
那么接下来我们就根据这个伪代码去映射我们自己写的MyPromise:
- 首先,在new Promise()时必须传入一个执行器,否则会报错,并且这个执行器是立即执行的,所以我们在constructor这里给一个形参executor,并且在constructor中立即执行
- 第二,在这个executor中需要传入两个回调函数,以便后面修改Promise的状态,所以我们要在this身上绑两个方法,并且这个两个方法要传给executor
- 第三,我们在resolve或者reject的时候一般都会传一个解决的结果或者失败的原因,所以我们还需要在this身上绑一个result来保存成功或者失败的值
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
constructor(executor){
this.status = PENDING
this.result = undefined
this._resolve = (value) => {
if (this.status !== PENDING) return
this.status = FULFILLED
this.result = value
}
this._reject = (err) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.result = err
}
executor(this._resolve, this._reject)
}
then(){}
catch(){}
static resolve(){}
static reject(){}
static all(){}
static race(){}
}
至此,我们差不多是完成了初始化Promise,我们可以简单测试一下下
const p1 = new MyPromise((resolve,reject)=>{
resolve(1)
reject(2)
})
console.log(p1)
目前来说没有什么大问题
then的实现
接下来就是最麻烦的then的实现了,只要这个懂了,其他的都是触类旁通。 老样子,我们还是先把原生的Promise的基本语法写了,然后我们再去映射我们自己的代码
const p1 = new Promise((resolve,reject)=>{
if(网络请求成功){
resolve(网络请求回来的数据)
}else{
reject(失败原因)
}
})
p1.then(console.log,console.log)
上面这个代码的意思就是成功了就打印成功的数据,失败了就打印失败的原因,console.log是个函数,如果Promise的状态变为resolve,那么它的.then就会去调用成功的回调,也就是第一个console.log
接下来我们就来映射一下我们的代码:
- 首先,then方法需要两个回调,那我们就在then中给两个回调,那这两个回调什么时候执行呢,是不是状态为成功的时候执行第一个回调,状态为失败就执行第二个回调,那就得在then方法中进行判断了。
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
constructor(executor){
this.status = PENDING
this.result = undefined
this._resolve = (value) => {
if (this.status !== PENDING) return
this.status = FULFILLED
this.result = value
}
this._reject = (err) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.result = err
}
executor(this._resolve, this._reject)
}
then(onFulfilled,onRejected){
if(this.status === FULFILLED){
onFulfilled(this.result)
}else if(this.status === REJECTED){
onRejected(this.result)
}
}
catch(){}
static resolve(){}
static reject(){}
static all(){}
static race(){}
}
虽然代码还有问题,但是我们还是可以简单测试一下:
const p1 = new MyPromise((resolve,reject)=>{
resolve(1)
reject(2)
})
p1.then(console.log,console.log)
看起来似乎没有什么问题,那是因为我们在初始化MyPromise时就已经把状态变为成功了,所以then的时候就安全通过了考验,如果我加个定时器,让它一秒以后再把状态变为成功的话,它就不会有任何打印了。这个要捋清楚,这是我们自己写的then,它一定是同步执行的!!!那就是说执行到then的时候状态还没变呢,then里面的判断一个都不符合现在的情况,所以就什么都不打印,那怎么让状态变了以后还可以执行到then的回调呢?那就只能把它先存起来,等到状态变了再把它拿出来执行对不对?
所以我们需要修改then方法的代码:
-
首先思考我得存在哪里?存的这个地方一定得是MyPromise的实例拿得到的,那么我只想到两种方法,第一是存全局,第二是存在this身上,很明显存在全局变量上是不妥的,因为我们可能需要多个MyPromise实例,这样就没法收集到所有的MyPromise实例的then回调
-
其次思考什么时候存?怎么存?我们上面分析了,当执行到then时,状态还是等待的话,后面再改变状态就没法执行回调,所以我们应该在状态为等待时存,那么我们怎么存?我们应该存在数组里面,因为在Promise中式可以多次执行then方法的,比如:
const p1 = new Promise((resolve,reject)=>{ if(网络请求成功){ resolve(网络请求回来的数据) }else{ reject(失败原因) } }) p1.then(console.log) p1.then(console.log)
说的专业点就是需要收集依赖,所以用数组会比较妥当
那么我们就根据上面的分析来修改then方法以及被牵连到的constructor:
constructor(executor) {
this.status = PENDING
this.resolveCallback = []
this.rejectCallback = []
this.result = undefined
this._resolve = (value) => {
if (this.status !== PENDING) return
this.status = FULFILLED
this.result = value
while (this.resolveCallback[0]) {
const callback = this.resolveCallback.shift()
callback(value)
}
}
this._reject = (err) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.result = err
while (this.rejectCallback[0]) {
const callback = this.rejectCallback.shift()
callback()
}
}
executor(this._resolve, this._reject)
}
then (onFulfilled, onRejected) {
if (this.status === PENDING) {
this.resolveCallback.push(onFulfilled)
this.rejectCallback.push(onRejected)
} else if (this.status === FULFILLED)
onFulfilled(this.result)
else if (this.status === REJECTED)
onRejected(this.result)
}
我们来测试一下,并且再分析一下整个流程:
const p1 = new MyPromise((resolve,reject)=>{
setTimeout(resolve,1000,2)
})
p1.then(console.log,console.log)
结果是没什么大问题,那么我们再来分析一下整个过程,防止后面完成then链式调用的时候晕掉
- 首先p1的状态在运行完MyPromise的时候为PENDING,定时器已经挂起定时了,这时候又遇到了then方法,进入then方法,判断现在的状态为PENDING,所以把成功的回调和失败的回调都推到this对于的队列中(数组),等待状态改变。
- 这时候定时器结束了,定时器的回调被推到宏任务队列中,主线程上又没有可以执行的代码了,所以宏任务队列中的回调被拉出来执行了,这个回调就是resolve(),既然resolve()了,那么就会执行this._resolve,那么就会把this的相应的队列中的依赖取出来执行,这时候状态为成功,并且成功的值为2,这个地方千万千万不要晕掉,看不懂就打断点debug,慢慢的就懂了。
then的链式调用
这个地方真的是这个部分的重难点了,所以说上面的部分一定要看懂看懂!!!
老规矩,我们还是分析一下原生的Promise是如何then链式调用的,再映射到我们的代码中去:
const p1 = new Promise((resolve,reject)=>{
if(网络请求成功){
resolve(网络请求回来的数据)
}else{
reject(失败原因)
}
})
p1.then(()=>{
return 1
}).then((data)=>{
console.log(data)//如果p1是成功的Promise,这里会输出1
return new Promise((resolve,reject)=>{
setTimeout(resolve,1000,2)
})
}).then((data)=>{
console.log(data)//一秒过后输出2
})
这里也就不赘述这个代码为什么这样了,还是先建议不了解Promise语法的朋友们,先去熟悉语法,再来实现一个自己的Promise!!!
OK,那么从上面代码可以看出then方法一定是返回一个Promise实例的,否则怎么链式调用呢,那我们还是一样先把架子搭起来,待会再一点一点解决bug:
then (onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
if (this.status === PENDING) {
this.resolveCallback.push(onFulfilled)
this.rejectCallback.push(onRejected)
}
else if (this.status === FULFILLED) fulfilledFn(this.result)
else if (this.status === REJECTED) rejectedFn(this.result)
})
}
变动不大,现在只能保证链式调用不会报错,但是还有很多问题要解决,我们一个一个来
- 下一个then要接收上一个then的返回值,这个返回值在onFulfilled或者onRejected里面,如果这里面返回的不是Promise对象,那么会被包装成一个已经解决的Promise对象,所以我们又要改这个then代码了
在改之前,我们要明确我们该如何拿到上一个then的返回值?拿到以后怎么办?还有一个最重要的问题就是,这一步应该什么时候运行?
首先,想要拿到一个函数的返回值就是运行它,其次我们拿到返回值以后需要判断这个返回值是否是MyPromise实例,如果是就不做处理,只需要等待它状态改变即可,如果不是就要把它包装成一个MyPromise对象,那么我们什么时候运行它呢?这里还是需要提醒一下,这是我们自己写的模仿Promise,then方法里面的代码是同步的,所以不能一上来就去拿返回值然后判断什么的,我们应该还是把它封装在一个函数里面,然后在状态改变的时候运行它,所以下面的代码修改幅度比较大,大家好好理解一下
then (onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const fulfilledFn = (value) => {
const res = onFulfilled(value)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
}
const rejectedFn = (err) => {
const res = onRejected(err)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
}
if (this.status === PENDING) {
this.resolveCallback.push(fulfilledFn)
this.rejectCallback.push(rejectedFn)
}
else if (this.status === FULFILLED) fulfilledFn(this.result)
else if (this.status === REJECTED) rejectedFn(this.result)
})
}
为了防止大家混乱,还是举个例子,然后过一下整个流程,防止越学越烦躁
const p1 = new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000, 2)
})
p1.then((data) => {
console.log(data)
return 2222
}).then((data) => {
console.log(data)
return new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000, 666)
})
}).then((data) => {
console.log(data)
})
- 首先我们p1是一个即将被解决的Mypromise实例,那么then方法可不惯着它这迟到的毛病,then方法该执行还是执行,当执行到p1.then(...)时,首先就是返回了一个MyPromise,我们就叫它then的返回值,那么这个then的返回值有自己的resolve和reject,全都已经初始化好了,就等着它自己改变状态,不改变状态的话,如何让下一个then的回调执行呢,对吧?
- 那再急也得先等等,等我判断一下你的返回值啊,是不是?谁的返回值啊?是不是p1.then()的两个回调的返回值嘛,那咋整,是不是得运行一下这两个回调啊,那人家现在p1还没被解决呢,你不能趁主人不在家,你顺手就抱了人家的仔吧。你不能运行啊,那咋办,只好等人家p1被解决了以后再运行呗,那就只能老办法,把它包在一个函数里面存起来,存在p1对应的成功的队列里面( this.resolveCallback.push(fulfilledFn)),只要p1一解决,立马就拿到返回值了
- 那么这个包起来的函数里面,究竟是个什么妖精呢?其实也就两句代码,第一句拿返回值(是then的回调的返回值),顺便就把成功的回调执行了(既然运行到这里,说明状态变了,状态变了就得运行then的回调)。拿到以后判断,你是不是MyPromise的实例啊?然后不是,不是就赶紧让人家then的返回值resolve()了啊,对不,好让人家执行下一个then的回调,那要是是Mypromise的实例呢?那得等啊,等啥?等人家实例状态改变啊,这个实例状态一改变,then的返回值的状态也得跟着改变,所以就干脆把then的返回值的resolve和reject当作回调传给这个实例的then,对不。
- 那么接下来的链式调用then也是和上面一样的步骤,还是一句话,看不懂就打断点,一步一步分析,千万不要放弃!!!
最后结果:
值穿透的问题
你以为结束了吗?🤪,还有一个值穿透的问题,当给then传的实参不是一个函数的时候,需要忽略掉,然后把值传下去,所以我们还得修改一下then方法
then (onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
typeof onFulfilled === 'function' ? null : onFulfilled = value => value
typeof onRejected !== 'function' ? onRejected = reason => {
throw new Error(reason instanceof Error ? reason.message : reason)
} : null
const fulfilledFn = (value) => {
const res = onFulfilled(value)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
}
const rejectedFn = (err) => {
try {
const res = onRejected(err)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
} catch (error) {
reject(error)
}
}
if (this.status === PENDING) {
this.resolveCallback.push(fulfilledFn)
this.rejectCallback.push(rejectedFn)
} else if (this.status === FULFILLED) fulfilledFn(this.result)
else if (this.status === REJECTED) rejectedFn(this.result)
})
}
这里理解起来就比较简单了,就是判断回调是否是个函数,是的话就啥都不做,不是的话就得让它变成一个函数,别让代码报错,同时还得把值传下去,如何把值传下去呢,就是得利用resolve(value),所以成功的回调和失败的回调处理方式不一样,失败了就是失败了,不能把失败的回调改成成功,所以需要抛出错误,然后捕获错误以后,再reject()
catch的实现
catch 其实你可以认为是语法糖,毕竟在then里面写两个回调然后又链式调用,看起来超累,那catch就弱化了这个问题,不需要在then里面写失败的回调,只需要加上一个catch即可
实现catch很简单,如果catch的上面的then都没有写失败的回调,那么我们刚才写的值穿透就派上用场了,它会直接把失败的值穿透到catch这里,接下来你也应该知道了,catch这里必须得有失败的回调了,所以就只需要写一行代码就好:
catch (reject) {
return this.then(null, reject)
}
各种静态方法的实现
接下来就是舒服的时候了,都是很简单的代码块
MyPromise.resolve的实现
当我MyPromise.resolve()时,返回的应该是一个MyPromise对象,并且需要判断传入的参数是不是MyPromise的实例,是的话就直接返回这个参数,不是的话就要执行resolve(),然后返回。
static resolve (value) {
if (value instanceof MyPromise) return value
return new MyPromise((resolve, reject) => {
resolve(value)
})
}
MyPromise.reject的实现
MyPromise.reject也是同理,只不过它和MyPromise.resolve有点不同,大家有时间可以看看红宝书,里面有提及到resolve是幂等方法,但是reject不是。
static reject (value) {
return new MyPromise((resolve, reject) => {
reject(value)
})
}
MyPromise.all的实现
这个方法顾名思义就是要等待全部的期约都解决了,整体的期约才是被解决的状态,只要有一个是失败,整体就是失败,下面的代码还是比较简单的,我就稍微解释一下,首先传入的参数必须是个可迭代对象,这里其实用for...of会更严谨,但是我写习惯了,脑子里想的for of,一写就变成foreach了。
首先遍历传进来的可迭代对象,如果其中有元素是非MyPromise的实例,默认就是成功的,所以干脆直接用MyPromise.resolve()全包起来。然后给每个元素都绑一个then函数,只要有一个是reject,那么all返回的MyPromise就是reject。那么给每个元素的then的成功回调里面都给一个判断,判断每次resolve的是不是最后一个,是的话就把all返回的MyPromise对象直接resolve(),当然成功的值得是所有MyPromise成功的值的集合,所以拿个数组存着。
static all (res) {
return new MyPromise((resolve, reject) => {
const fulfilledArr = []
let i = 0
res.forEach(el => {
MyPromise.resolve(el).then(
(value) => {
fulfilledArr.push(value)
i++
if (i === res.length) {
resolve(fulfilledArr)
}
},
(err) => {
reject(err)
})
})
})
}
MyPromise.race的实现
这个就更简单了,和上面的一样,只不过这是比赛,看谁的状态先改变,那么race返回的MyPromise的状态就跟着它改变
static race (res) {
return new MyPromise((resolve, reject) => {
res.forEach(el => {
MyPromise.resolve(el).then(resolve, reject)
})
})
}
小细节
对了,这里我没有去模拟微任务队列,反正模拟它最简单的方法也就是用个定时器,最后还是和Promise有不一样的地方,所以就是大家想模拟的话就在依赖队列被拉出来执行的那里加个定时器模拟一下就行了(其实就是我忘记了,然后写累了不想写了哈哈哈哈)
完整代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor(executor) {
this.status = PENDING
this.resolveCallback = []
this.rejectCallback = []
this.result = undefined
this._resolve = (value) => {
if (this.status !== PENDING) return
this.status = FULFILLED
this.result = value
while (this.resolveCallback[0]) {
const callback = this.resolveCallback.shift()
callback(value)
}
}
this._reject = (err) => {
if (this.status !== PENDING) return
this.status = REJECTED
this.result = err
while (this.rejectCallback[0]) {
const callback = this.rejectCallback.shift()
callback()
}
}
executor(this._resolve, this._reject)
}
then (onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
typeof onFulfilled === 'function' ? null : onFulfilled = value => value
typeof onRejected !== 'function' ? onRejected = reason => {
throw new Error(reason instanceof Error ? reason.message : reason)
} : null
const fulfilledFn = (value) => {
const res = onFulfilled(value)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
}
const rejectedFn = (err) => {
try {
const res = onRejected(err)
res instanceof MyPromise ? res.then(resolve, reject) : resolve(res)
} catch (error) {
reject(error)
}
}
if (this.status === PENDING) {
this.resolveCallback.push(fulfilledFn)
this.rejectCallback.push(rejectedFn)
} else if (this.status === FULFILLED) fulfilledFn(this.result)
else if (this.status === REJECTED) rejectedFn(this.result)
})
}
catch (reject) {
return this.then(null, reject)
}
static resolve (value) {
if (value instanceof MyPromise) return value
return new MyPromise((resolve, reject) => {
resolve(value)
})
}
static reject (value) {
return new MyPromise((resolve, reject) => {
reject(value)
})
}
static all (res) {
return new MyPromise((resolve, reject) => {
const fulfilledArr = []
let i = 0
res.forEach(el => {
MyPromise.resolve(el).then(
(value) => {
fulfilledArr.push(value)
i++
if (i === res.length) {
resolve(fulfilledArr)
}
},
(err) => {
reject(err)
})
})
})
}
static race (res) {
return new MyPromise((resolve, reject) => {
res.forEach(el => {
el.then(resolve, reject)
})
})
}
}
好了,写的好累,终于写完了,如果看到这了,说明你真的很棒!!!