手写Promise
初始结构
我们在 New
一个 Promise
里的时候肯定是需要传入参数的,不然这个实例用处不大,而这个参数我们知道是一个函数,而且当我们传入这个函数参数的时候,这个函数参数会被自动执行。
因此我们需要在类的 constructor
里面添加一个参数,这里就用
func
来作为形参,并且执行一下这个参数,接下来需要为这个函数参数传入他自己的参数,也就是
resolve
和 reject
,原生的 Promise 里面可以传入
resolve
,reject
两个参数,那么我们也得允许手写这边可以传入这两个参数。
class Promise {
constructor(func) {
func(resolve,reject);
}
}
但是这样写明显有问题,因为手写这边不知道在哪里调用
resolve
和 reject
这两个参数,毕竟
resolve
和 reject
还没有定义。因此,就需要创造出这两个对象,有一点我们要知道的是,resolve
、reject
也是以函数的形式来执行的,我们在原生 Promise
里也是在
resolve
或者 reject
后面加个括号来执行的,因此我们可以用类方法的形式,来创建这两个函数。
class Promise {
constructor(func) {
func(this.resolve,this.reject);
}
resolve() {}
reject() {}
}
那么这里的 resolve
,reject
方法应该如何执行呢?里面应该写什么内容呢。这就需要用到状态了。
Promise有三种状态,分别是 pending
,fulfilled
和 rejected
。初始的时候是
pending
,pending
可以转为
fulfilled
状态,但是不能逆转,pending
也可以转为 rejected
状态,但是也不能逆转。fulfilled
和 rejected
之间也不能互转
因此,需要提前先把这些状态定义好,可以用const来创建外部的固定变量,但是这里为了统一,就用
static
来创建静态属性,创建了状态属性以后,还需要为每一个实例添加一个状态属性,这里就用
this.status
,这个状态属性默认就是待定状态
。这样在每一个实例被创建以后,就会有自身的状态属性可以进行判断和变动了
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
func(this.resolve,this.reject);
}
resolve() {}
reject() {}
}
那么在执行 resolve
的时候,就需要判断状态是否为待定,如果是待定的话,就把状态改为成功;同样的道理,在执行
reject
时候,就需要判断状态是否为待定,如果是待定的话,就把状态改为拒绝。
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
func(this.resolve,this.reject);
}
resolve() {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
}
}
reject() {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
}
}
}
再回忆一下原生 Promise
,在执行 resolve
或者
reject
的时候,都是可以传入一个参数,这样我们后面就可以使用这个参数了。
let Promise = new Promise((resolve, reject) => {
resolve('zepoch');
})
我们可以把这个结果参数命名为
result
,不管是成功还是拒绝的结果,两者选其一我们让每个实例都有
result
属性,并且给他们都负值 null
,这里给空值
null
是因为执行 resolve 或者 reject
的时候会给结果赋值,接着我们就可以给 resolve
添加参数,并且把参数负值给实例的 result
属性,为
reject
添加参数,并且为参数负值给实例resort属性。
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
this.result = null;
func(this.resolve,this.reject);
}
resolve(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
}
}
reject(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
}
}
}
this 指向
但是此时却出现了一些问题,但是从报错的信息里面我们貌似发现不了有什么错误,因为
status
属性我们已经创建了,不应该是
undefined
,但我们仔细看看 status
,前面是有
this
关键字的,那么只有一种可能,要用
this.status
的时候并没有调用 constructor
里的
this.status
,也就是这里的 this
已经跟丢了。我们在 new
一个新实例的时候,执行的是
constructor
里的内容,也就是 constructor
里的
this
确实是新实例的,但现在我们是在新实例被创建后,再在外部环境下执行
resolve
方法的,这里的 resolve
看着像是和实例一起执行的,其实不然,也就相当于不在 class
内部使用这个 this
,而我们。没有在外部定义任何
status
变量,因此这里会报错。解决 class
的
this
指向问题,一般会用箭头函数
,bind
或者
proxy
,在这里我们就可以使用 bind
来绑定
this
。只需要在 this.resolve
和
this.reject
后加上
bind(this)
,刷新之后便不报错了
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
this.result = null;
func(this.resolve.bind(this),this.reject.bind(this));
}
resolve(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
}
}
reject(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
}
}
}
对 resolve
来说,这里就是给实例的 resolve
方法,绑定这个 this
为当前实例对象,并且执行
this.resolve
方法;对于 reject
来说,这里就是给实例的 reject
方法绑定这个
this
为当前的实例对象,并且执行 this.reject
方法。
then
原生的 then
方法,then
方法可以传入两个参数,这两个参数都是函数,一个是当状态为成功时执行的代码,另一个是当状态为拒绝时执行的代码
let promise = new Promise((resolve, reject) => {
resolve('zepoch');
reject('zepoch');
});
promise.then(
result => {
console.log(result);
},
result => {
console.log(result);
}
)
因此我们就可以先给手写的店里面添加两个参数。一个是
onFULFILLED
,表示状态为成功时,另一个是
onREJECTED
,表示状态为拒绝时,这里我们先看看原生
Promise
产生的结果。
可以看到控制台只显示了一个 console
的结果。证明只会执行成功状态或者拒绝状态其中一个,因此我们在手写的时候就必须进行判断。如果当前实力的stands状态属性为成功的话,我们就执行传进来的
onFULFILLED
函数,并且为 onFULFILLED
函数传入前面保留的 result
属性值,如果当前实例的
status
状态属性为拒绝的话。我们就执行传进来的onREJECTED
函数,并且为 onREJECTED
函数传入前面保留的
result
属性值。
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
this.result = null;
func(this.resolve.bind(this),this.reject.bind(this));
}
resolve(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
}
}
reject(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
}
}
then(onFULFILLED, onREJECTED) {
if(this.status === Promise.FULFILLED) {
onFULFILLED(this.result);
}
if(this.status === Promise.REJECTED) {
onREJECTED(this.result);
}
}
}
定义好了判断条件以后,我们就来测试一下代码。也是一样,在实例上使用
then
方法,我们来看看控制台,会发现这里并没有报错,也就是暂时安全了。
为什么说暂时安全了呢?手写 Promise
的时候,有一个难点,就在于有很多地方需要和原生一样严谨。也就是说,原生的
Promise
会考虑很多特殊情况,我们在实际运用时可能暂时不会碰到这些情况,可是当我们遇到的时候,却不知底层的原理,这就是为什么我们要知道如何手写
Promise
。
执行异常
如果在 new Promise
的时候。执行函数里面我们抛出错误,是会触发拒绝
方法,也就是在原生的
promise
里面调用 then
方法时可以把错误的信息作文内容输出出来
但是如果我们在手写这边写上同样道理的代码,很多人会忽略这个细节,我们看看控制台。这个时候就是报错了,而且没有把内容输出出来。
于是我们就可以在执行 resolve
和 reject
之前进行判断,可以用 try
和 catch
在
constructor
里面完善代码,当生成实力的时候判断是否有报错,如果没有报错的话就按照正常执行
resolve
和 reject
方法,如果报错的话,就把错误信息传入给 reject
方法,并且直接执行 reject
方法。注意,这里不需要给
reject
方法进行 this
的绑定了,因为这里是直接执行而不是创建实例后再执行。现在我们再刷新一下控制台,就能看出手写这边没有报错了。
原生 Promise
里规定 then
里面的两个参数,如果不是函数的话就要被忽略,所以需要把不是函数的参数改为函数,这里我们就可以用条件运算符,我们在进行if判断之前。进行预先判断,如果
onFULFILLED
参数是一个函数,就把原来的
onFULFILLED
的内容重新复制给他,如果
onFULFILLED
参数不是一个函数,就把它改为空函数,如果
onREJECTED
参数。是一个函数,就把原来的
onREJECTED
的内容重新复制给他,如果 onREJECTED
参数不是一个函数。就把它改为空函数,现在我们再来查看一下控制台的时候,就没有发现报错了。
异步
在手写代码里面。依旧没有植入异步功能,毕竟最基本的
setTimeout
我们都没有使用,但是我们必须先了解一下原生
Promise
的一些运行顺序规则。
我们配合这段原生 Promise
代码结合控制台一起看看,首先执行第一步,接着创建 promise
实例并且输出第二步,因为这里依旧是同步,接着碰到 resolve
的时候,修改结果,值到了 promise.then
会进行异步操作,也就是我们需要先把执行栈的内容清空,于是就执行第三步,接着才会执行
promise.then
里面的内容,也就是最后输出zepoch
。
我们用同样的测试代码应用在手写代码上面,也就是在手写代码写上步骤的信息,然后node运行
这次我们发现有些不同了,第一第二步都没有问题,问题就是zepoch
和第三步,这里的顺序不对。其实问题很简单,就是我们刚刚说的没有设置异步执行,所以直接给
then
方法里面添加 setTimeout
就可以了,我们需要在进行if判断以后再添加
setTimeout
,要不然状态不符合添加异步也是没有意义的,然后在
setTimeout
里执行传入的函数参数。
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
this.result = null;
try {
func(this.resolve.bind(this),this.reject.bind(this));
} catch(error) {
this.reject(error);
}
}
resolve(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
}
}
reject(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
}
}
then(onFULFILLED, onREJECTED) {
onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {};
onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {};
if(this.status === Promise.FULFILLED) {
setTimeout(() => {
onFULFILLED(this.result);
})
}
if(this.status === Promise.REJECTED) {
setTimeout(() => {
onREJECTED(this.result);
})
}
}
}
现在我们看看控制台。这次的顺序就比较顺眼了
不过异步的问题真的解决了吗?现在又要进入 Promise
另一个难点了,我们来给原生的 Promise
里添加
setTimeout
,使得 resolve
也进行异步执行,那么就会出现一个问题了,resolve
是异步的,then
也是异步的,究竟谁会先被调用呢?
我们看看控制台,步骤是按照我标注的正常顺序来的。特别要注意的是,当遇到
setTimeout
的时候被异步执行了,而
resolve('zepoch')
没有被马上执行,而是先执行第四步,等到
then
的时候再执行 resolve
里保存的值。
我们用同样的代码应用到手写的部分,先来看看控制台。
可以发现zepoch
并没有输出,我们可以。先猜测一下没有输出的原因,很可能是因为
then
方法没有被执行,看看 then
方法里面是根据条件判断来执行代码的,也就是说。很可能没有符合的条件,再换句话说,可能没有符合的状态,那么我们就在三个位置分别输出当前的状态,这样分别来判断哪个位置出了问题。
现在在看看控制台,发现。只有两组状态被输出,这两组都在第四步前被输出了。证明
setTimeout
里面的状态都被输出了,只有 then
里面的状态没有被输出,那基本就能确定是因为 then
里面的状态判断出了问题。
执行第一,第二,第三步的时候,就要开始处理异步了。这里肯定是因为先执行了
then
方法又发现这个时候状态依旧是待定,而我们手写部分没有定义待定状态的时候应该做什么,因此。就少了
zepoch
这句话的输出了,所以我们就直接给 then
方法里面添加待定状态的情况就可以了,也就是用 if
进行判断。
then(onFULFILLED, onREJECTED) {
onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {};
onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {};
if(this.status === Promise.PENDING) {
this.resolveCallbacks.push(onFULFILLED);
this.rejectCallbacks.push(onREJECTED);
}
if(this.status === Promise.FULFILLED) {
setTimeout(() => {
onFULFILLED(this.result);
})
}
if(this.status === Promise.REJECTED) {
setTimeout(() => {
onREJECTED(this.result);
})
}
}
但是问题来了。当在里面判断到待定状态时,我们要干什么? 因为这个时候
resolve
或者 reject
还没有获取到任何值。因此,我们必须让 then
里的函数稍后再执行的,等 resolve
执行了以后,再执行
then
,为了保留 then
里的函数,我们可以创建数组来保存函数。
this.resolveCallbacks = [];
this.rejectCallbacks = [];
在实例化对象的时候就让每一个实例都有这两个数组,一个数组保存
resolve
函数,另一个数组保存 reject
函数,为什么是数组呢?因为数组是先入先出的顺序,接着就完善
then
里面的代码,也就是当判断到状态为待定时,暂且把
then
里的两个函数参数分别放在两个数组里面,数组里面放完函数以后就可以完善
resolve
和 reject
代码了。 在执行
resolve
或者 reject
的时候,遍历自身的
callback
数组。看看数组里面有没有 then
那边保留过来的待执行函数,然后逐个执行数组里面的函数,执行的时候会传入相应的参数
resolve(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
this.resolveCallbacks.forEach(callback => {
callback(result)
})
}
}
reject(result) {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
this.rejectCallbacks.forEach(callback => {
callback(result)
})
}
}
然后我们修改一下实例里面的代码,并且同时用 resolve
和
reject
,为了看看是否会同时出现两种状态的值,是否有这样的错误。结果我们可以看到代码顺序,还是不太对。
这里有个小细节,resolve
和 reject
是要在事件循环末尾执行的,因此我们就给 resolve
和
reject
里面加上
setTimeout
,然后把原来的代码复制上去就可以了,现在再来看看控制台,就会发现没有错误了。
首先进行第一步,然后 new
一个实例进行,第二步遇到
setTimeout
的时候进行异步操作,然后运行实例的
then
方法,发现依旧是待定状态,就把函数参数放到数组里面保存起来。然后进行第三步,现在又要回头去执行刚刚
setTimeout
里面的内容,要执行 resolve
的时候发现又要 setTimeout
异步处理,于是就执行第四步,最后再来执行
resolve
,也就是改变状态,改变结果值,并且遍历刚刚保存的数组对象,最后执行刚刚保存的函数对象,然后就输出
zepoch
了
链式
现在我们已经越来越接近胜利了,我修改一下代码,来看一下
promise
的链式功能。也就是 then
后面又有一个
then
,毫无疑问在控制台里面是会报错的
为了可以实现链式功能,我们需要让 then
方法返回一个新的
promise
,返回一个新的 promise
以后。他就有自己的 then
方法,这样就能实现无限的链式,现在我们就在 then
方法里面返回一个新的手写 Promise
实例,再把原来的代码复制上去就可以了。
then(onFULFILLED, onREJECTED) {
return new Promise((resolve,reject) => {
onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {};
onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {};
if(this.status === Promise.PENDING) {
this.resolveCallbacks.push(onFULFILLED);
this.rejectCallbacks.push(onREJECTED);
}
if(this.status === Promise.FULFILLED) {
setTimeout(() => {
onFULFILLED(this.result);
})
}
if(this.status === Promise.REJECTED) {
setTimeout(() => {
onREJECTED(this.result);
})
}
})
}
全部代码如下
class Promise {
static PENDING = '待定';
static FULFILLED = '成功';
static REJECTED = '拒绝';
constructor(func) {
this.status = Promise.PENDING;
this.result = null;
this.resolveCallbacks = [];
this.rejectCallbacks = [];
try {
func(this.resolve.bind(this),this.reject.bind(this));
} catch(error) {
this.reject(error);
}
}
resolve(result) {
setTimeout(() => {
if (this.status === Promise.PENDING) {
this.status = Promise.FULFILLED;
this.result = result;
this.resolveCallbacks.forEach(callback => {
callback(result)
})
}
})
}
reject(result) {
setTimeout(() => {
if (this.status === Promise.PENDING) {
this.status = Promise.REJECTED;
this.result = result;
this.rejectCallbacks.forEach(callback => {
callback(result)
})
}
})
}
then(onFULFILLED, onREJECTED) {
return new Promise((resolve,reject) => {
onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {};
onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {};
if(this.status === Promise.PENDING) {
this.resolveCallbacks.push(onFULFILLED);
this.rejectCallbacks.push(onREJECTED);
}
if(this.status === Promise.FULFILLED) {
setTimeout(() => {
onFULFILLED(this.result);
})
}
if(this.status === Promise.REJECTED) {
setTimeout(() => {
onREJECTED(this.result);
})
}
})
}
}