初始结构

我们在 New 一个 Promise 里的时候肯定是需要传入参数的,不然这个实例用处不大,而这个参数我们知道是一个函数,而且当我们传入这个函数参数的时候,这个函数参数会被自动执行。 因此我们需要在类的 constructor 里面添加一个参数,这里就用 func 来作为形参,并且执行一下这个参数,接下来需要为这个函数参数传入他自己的参数,也就是 resolvereject,原生的 Promise 里面可以传入 resolvereject 两个参数,那么我们也得允许手写这边可以传入这两个参数。

class Promise {
    constructor(func) {
        func(resolve,reject);
    }
}

但是这样写明显有问题,因为手写这边不知道在哪里调用 resolvereject 这两个参数,毕竟 resolvereject 还没有定义。因此,就需要创造出这两个对象,有一点我们要知道的是,resolvereject 也是以函数的形式来执行的,我们在原生 Promise 里也是在 resolve 或者 reject 后面加个括号来执行的,因此我们可以用类方法的形式,来创建这两个函数。

class Promise {
    constructor(func) {
        func(this.resolve,this.reject);
    }
    resolve() {}
    reject() {}
}

那么这里的 resolvereject 方法应该如何执行呢?里面应该写什么内容呢。这就需要用到状态了。 Promise有三种状态,分别是 pendingfulfilledrejected。初始的时候是 pendingpending 可以转为 fulfilled 状态,但是不能逆转,pending 也可以转为 rejected 状态,但是也不能逆转。fulfilledrejected 之间也不能互转

20220129134733

因此,需要提前先把这些状态定义好,可以用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;
        }
    }
}

20220129172325

this 指向

但是此时却出现了一些问题,但是从报错的信息里面我们貌似发现不了有什么错误,因为 status 属性我们已经创建了,不应该是 undefined,但我们仔细看看 status,前面是有 this 关键字的,那么只有一种可能,要用 this.status 的时候并没有调用 constructor 里的 this.status,也就是这里的 this 已经跟丢了。我们在 new 一个新实例的时候,执行的是 constructor 里的内容,也就是 constructor 里的 this 确实是新实例的,但现在我们是在新实例被创建后,再在外部环境下执行 resolve 方法的,这里的 resolve 看着像是和实例一起执行的,其实不然,也就相当于不在 class 内部使用这个 this,而我们。没有在外部定义任何 status 变量,因此这里会报错。解决 classthis 指向问题,一般会用箭头函数bind或者 proxy,在这里我们就可以使用 bind 来绑定 this。只需要在 this.resolvethis.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;
        }
    }
}

20220129172340

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 产生的结果。

20220129205912

可以看到控制台只显示了一个 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 方法,我们来看看控制台,会发现这里并没有报错,也就是暂时安全了。

20220129210813

为什么说暂时安全了呢?手写 Promise 的时候,有一个难点,就在于有很多地方需要和原生一样严谨。也就是说,原生的 Promise 会考虑很多特殊情况,我们在实际运用时可能暂时不会碰到这些情况,可是当我们遇到的时候,却不知底层的原理,这就是为什么我们要知道如何手写 Promise

执行异常

如果在 new Promise 的时候。执行函数里面我们抛出错误,是会触发拒绝方法,也就是在原生的 promise 里面调用 then 方法时可以把错误的信息作文内容输出出来

20220129211639

但是如果我们在手写这边写上同样道理的代码,很多人会忽略这个细节,我们看看控制台。这个时候就是报错了,而且没有把内容输出出来。

20220129211754

于是我们就可以在执行 resolvereject 之前进行判断,可以用 trycatchconstructor 里面完善代码,当生成实力的时候判断是否有报错,如果没有报错的话就按照正常执行 resolvereject 方法,如果报错的话,就把错误信息传入给 reject 方法,并且直接执行 reject 方法。注意,这里不需要给 reject 方法进行 this 的绑定了,因为这里是直接执行而不是创建实例后再执行。现在我们再刷新一下控制台,就能看出手写这边没有报错了。

20220129212346

原生 Promise 里规定 then 里面的两个参数,如果不是函数的话就要被忽略,所以需要把不是函数的参数改为函数,这里我们就可以用条件运算符,我们在进行if判断之前。进行预先判断,如果 onFULFILLED 参数是一个函数,就把原来的 onFULFILLED 的内容重新复制给他,如果 onFULFILLED 参数不是一个函数,就把它改为空函数,如果 onREJECTED 参数。是一个函数,就把原来的 onREJECTED 的内容重新复制给他,如果 onREJECTED 参数不是一个函数。就把它改为空函数,现在我们再来查看一下控制台的时候,就没有发现报错了。

异步

在手写代码里面。依旧没有植入异步功能,毕竟最基本的 setTimeout 我们都没有使用,但是我们必须先了解一下原生 Promise 的一些运行顺序规则。

20220129214909

我们配合这段原生 Promise 代码结合控制台一起看看,首先执行第一步,接着创建 promise 实例并且输出第二步,因为这里依旧是同步,接着碰到 resolve 的时候,修改结果,值到了 promise.then 会进行异步操作,也就是我们需要先把执行栈的内容清空,于是就执行第三步,接着才会执行 promise.then 里面的内容,也就是最后输出zepoch。 我们用同样的测试代码应用在手写代码上面,也就是在手写代码写上步骤的信息,然后node运行

20220129215225

这次我们发现有些不同了,第一第二步都没有问题,问题就是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);
            })
        }
    }
}

现在我们看看控制台。这次的顺序就比较顺眼了

20220129215855

不过异步的问题真的解决了吗?现在又要进入 Promise 另一个难点了,我们来给原生的 Promise 里添加 setTimeout,使得 resolve 也进行异步执行,那么就会出现一个问题了,resolve 是异步的,then也是异步的,究竟谁会先被调用呢?

我们看看控制台,步骤是按照我标注的正常顺序来的。特别要注意的是,当遇到 setTimeout 的时候被异步执行了,而 resolve('zepoch')没有被马上执行,而是先执行第四步,等到 then 的时候再执行 resolve 里保存的值。

20220129220439

我们用同样的代码应用到手写的部分,先来看看控制台。

20220129220904

可以发现zepoch并没有输出,我们可以。先猜测一下没有输出的原因,很可能是因为 then 方法没有被执行,看看 then 方法里面是根据条件判断来执行代码的,也就是说。很可能没有符合的条件,再换句话说,可能没有符合的状态,那么我们就在三个位置分别输出当前的状态,这样分别来判断哪个位置出了问题。

20220129221345

现在在看看控制台,发现。只有两组状态被输出,这两组都在第四步前被输出了。证明 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 里的两个函数参数分别放在两个数组里面,数组里面放完函数以后就可以完善 resolvereject 代码了。 在执行 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)
        })
    }
}

然后我们修改一下实例里面的代码,并且同时用 resolvereject,为了看看是否会同时出现两种状态的值,是否有这样的错误。结果我们可以看到代码顺序,还是不太对。

20220130001359

这里有个小细节,resolvereject 是要在事件循环末尾执行的,因此我们就给 resolvereject 里面加上 setTimeout,然后把原来的代码复制上去就可以了,现在再来看看控制台,就会发现没有错误了。

20220130224337

首先进行第一步,然后 new 一个实例进行,第二步遇到 setTimeout 的时候进行异步操作,然后运行实例的 then 方法,发现依旧是待定状态,就把函数参数放到数组里面保存起来。然后进行第三步,现在又要回头去执行刚刚 setTimeout 里面的内容,要执行 resolve 的时候发现又要 setTimeout 异步处理,于是就执行第四步,最后再来执行 resolve,也就是改变状态,改变结果值,并且遍历刚刚保存的数组对象,最后执行刚刚保存的函数对象,然后就输出 zepoch

链式

现在我们已经越来越接近胜利了,我修改一下代码,来看一下 promise 的链式功能。也就是 then 后面又有一个 then,毫无疑问在控制台里面是会报错的

20220130225239

为了可以实现链式功能,我们需要让 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);
                })
            }
        })
        
    }
}