面试官:知道手写 Promise 吗?

291 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

--前言--

Promise在面试里一直是一个至关重要的一道坎,而它的实现原理在面试里出现的概率更是无限趋近于百分之一百,可想而知Promise的重要性。

在面试里,经常会要求我们手写promise.then或者promise.catch等等,但是要知道的是它们的实现原理都类似,所以这里咱们主要以讲怎么手写promise.then为主。(当然,不是A+规范)

第一步:定义一个Promise构造函数

我们都知道,Promise 是一个构造函数,那么我们第一步当然是要来定义一个这样的构造函数;

不过为了与原本的Promise构造函数进行区分,咱们这里定义的构造函数叫MyPromise构造函数。

//这里我们在MyPromise构造函数外面套了一层自执行函数,目的是习惯不把这样的构造函数的作用域暴露在太外层,所以就算不加上这外面的自执行函数也一样。
(function(window){
    function MyPromise(){
        
    }
    window.MyPromise = MyPromise    //将MyPromise挂载到window上(浏览器可执行,node引擎不支持)
})(window)

那么定义了一个MyPromise构造函数之后咱们就要考虑 .then 该如何实现了;

要让MyPromise构造函数定义出来的实例对象可以使用 .then ;

那么我们就要把 .then 定义到 MyPromise 的原型(prototype)上。

代码增添过如下:

(function(window){

    MyPromise.prototype.then = function(){    //将.then挂载到原型上
        
    }
    MyPromise.prototype.catch = function(){
        
    }
    
    function MyPromise(){
        
    }
    window.MyPromise = MyPromise    
})(window)

那么这个基本的架构就已经出来了。

第二步:构造 .then 的回调

.then 的回调? .then 有哪些回调?

诺,这就是原本官方的的 Promise 函数里 .then 可以执行的回调:

let p = new Promise((resolve,reject)=>{
    resolve('ok');
    reject('no');
})

p.then(
    (res) =>{
        console.log('res',res);
    },
    (error) =>{
        console.log('error',error);
    }
)

如果 Promise 的实例对象里用的是 resolve 函数,那么它的 .then 里就会走 res 的那个回调函数;

image.png

如果走的是 reject 函数,那么它的 .then 里将会走的就是 error 那个回调函数了。(效果等同于catch)

image.png

那么观望完官方的回调,我们也继续开始完善我们的 MyPromise 吧!

既然官方有给 .then 放置两个回调,那么我们就在 .then 的函数上加两个形参 onResolve 和 onReject。

同时顺便给 .catch 加上一个 onReject 回调.

(function(window){

    MyPromise.prototype.then = function(onResolve,onReject){
        
    }
    MyPromise.prototype.catch = function(onReject){
        
    }
    
    function MyPromise(){
        
    }
    window.MyPromise = MyPromise    
})(window)

第三步:给 MyPromise 构造函数加上 resolve 和 reject 函数方法

代码更新实现如下:

(function(window){

    MyPromise.prototype.then = function(onResolve,onReject){
        
    }
    MyPromise.prototype.catch = function(onReject){
        
    }
    
    function MyPromise(executor){
        function resolve(){
        }
        function reject(){
        }
        
        executor(resolve,reject)    //将resolve 和 reject 作为实参传给实例对象的回调,然后调用掉
    }
    
    window.MyPromise = MyPromise    
})(window)

同时,这里咱们放上一个执行器(其实也就是一个函数),将 resolve 和 reject作为实参传给它的实例对象的回调;

第四步:添加 Promise 状态

Promise 有三种状态,分别是 pending 、 fulfilled 以及 rejected 状态,

当然 fulfilled 状态我们一般更习惯叫 resolved 状态,

它们分别对应着初始 Promise 的状态的 pending 状态

resolve 方法改变 Promise 状态之后的 resolved 状态

以及 reject 方法改变 Promise状态之后的 rejected 状态

(function(window){

    MyPromise.prototype.then = function(onResolve,onReject){
        //把回调存放在callbacks数组里去
        let obj = {};
        obj.onResolve = onResolve;
        ovj.onReject = onReject;
        this.callbacks.push(obj)
    }
    MyPromise.prototype.catch = function(onReject){
        
    }
    
    function MyPromise(executor){
        let self = this          //将MyPromise里的指针赋值出来使用;
        self.status = 'pending'       //记录此时我们的Promise状态,初始状态为'pending';
        self.data = undefined         //定义一个存储变量,为将来传参的存储做准备;
        self.callbacks = []           //定义一个数组,存储触发的是 resolve 回调还是 reject 回调;
    
        function resolve(value){
            if(self.status !== 'pending'){
                return            //如果MyPromise不是初始状态pending,那么我们直接结束,因为状态改变后就不可逆了
            }
            self.status = 'resolved'    //变更MyPromise状态;
            self.data = value           //将传进来的实参储存起来
            //有没有待执行的callback函数
            if(self.callbacks.length>0){
                setTimeout(()=>{     //放置定时器为了让里面的代码变成宏任务,防止resolve执行完之后执行 .then 发现数组是空的这种情况,故强行定时器让两者保持先后执行
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onResolve(self.data)     //进行传参
                    })
                })
            }
        }
        function reject(){
            if(self.status !== 'pending'){
                return            //如果MyPromise不是初始状态pending,那么我们直接结束,因为状态改变后就不可逆了
            }
            self.status = 'rejected'    //变更MyPromise状态;
            self.data = value           //将传进来的实参储存起来
            //有没有待执行的callback函数
            if(self.callbacks.length>0){
                setTimeout(()=>{     //放置定时器为了让里面的代码变成宏任务,防止reject执行完之后执行 .then 发现数组是空的这种情况,故强行定时器让两者保持先后执行
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onReject(self.data)     //进行传参
                    })
                })
            }
        }
        
        executor(resolve,reject)
    }
    
    window.MyPromise = MyPromise    
})(window)

第五步:处理中间报错之后reject会不会去执行

官方的 Promise 是有这样一个特点的,如果代码在 Promise 对象里还未执行到 resolve 或者 reject 函数方法就出了错误或者bug,那么它仍然会执行reject进行报错;

你是不是不相信?那么咱们一起来看一下。

执行以下代码:

let p = new Promise((resolve,reject)=>{
    throw Error('err')
})

p.then(
    (res) =>{
        console.log('res',res);
    },
    (error) =>{
        console.log('error',error);
    }
)

p.catch(
    (err) => {
        console.log(err,'err')
    }
)

将会出现两个错误,一个是咱们人为抛出的错误,还有一个则是reject里返回的错误,如下图:

这说明什么?说明即便是 Promise 里面的代码出了问题, reject 也仍旧会执行。

image.png

那么咱们也要更加完善,把这部分功能加上。

即将executor(resolve,reject)的调用用异常抛出包裹

try {
    executor(resolve, reject)
} catch (error) {
    reject(error)
}

但是写到这里还是有问题的,因为当执行到 .then 的时候,可能 MyPromise 实例出来的对象状态是'pending', 也可能是 'resolved';而且还存在异步问题,resolve方法里面的判断语句if(self.callbacks.length>0) 也不可能成立

第六步:处理执行到 .then 时,MyPromise状态问题

这里我们只需要将 MyPromise 原型上的 then 函数里的代码改成以下即可:

MyPromise.prototype.then = function(onResolve,onReject){
    let self = this;
    if(self.status === 'pending'){
        self.callbacks.push({
            onResolve,
            onReject
        })
    }else if(self.status === 'resolved'){
        setTimeout(() => {
            onResolve(self.data)
        })
    }else{
        setTimeout(() => {
            onReject(self.data)
        })
    }
}

将以上代码补充入整份代码后,完善后完整代码如下:

(function(window){

    MyPromise.prototype.then = function(onResolve,onReject){
        let self = this;
        if(self.status === 'pending'){
            self.callbacks.push({
                onResolve,
                onReject
            })
        }else if(self.status === 'resolved'){
            setTimeout(() => {
                onResolve(self.data)    //如果执行不到resolve里面的判断,则自己将其值传进来
            })
        }else{
            setTimeout(() => {
                onReject(self.data)
            })
        }
    }
    MyPromise.prototype.catch = function(onReject){
        
    }
    
    function MyPromise(executor){
        let self = this 
        self.status = 'pending' 
        self.data = undefined 
        self.callbacks = []
    
        function resolve(value){
            if(self.status !== 'pending'){
                return
            }
            self.status = 'resolved'
            self.data = value     
            if(self.callbacks.length>0){
                setTimeout(()=>{
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onResolve(self.data)
                    })
                })
            }
        }
        function reject(){
            if(self.status !== 'pending'){
                return 
            }
            self.status = 'rejected'
            self.data = value
            if(self.callbacks.length>0){
                setTimeout(()=>{
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onReject(self.data) 
                    })
                })
            }
        }
        
        executor(resolve,reject)
    }
    
    window.MyPromise = MyPromise    
})(window)

将这份代码拿到浏览器上去执行,效果如下, .then 里是可以打印出来我们向 resolve 传的值的:

image.png

那么第一大步我们就算是做完了,剩下的就是完成如何在 .then 后面继续接 .then ,让其可以一直接 .then

第七步:实现.then 后继续接 .then

我们知道在官方的 .then 里面其实是会隐式的返回出来一个 Promise 对象的,

所以我们这里要做的第一步就是不管三七二十一先在 .then 里面返回出来一个 MyPromise 对象。

再在里面做操作。

MyPromise.prototype.then = function(onResolve,onReject){
    let self = this;
    return new MyPromise((resolve,reject) =>{
        if(self.status === 'pending'){
            self.callbacks.push({
                onResolve,
                onReject
            })
        }else if(self.status === 'resolved'){
            setTimeout(() => {
                const result = onResolve(self.data)
                if(result instanceof MyPromise){ //.then里的回调函数没有额外的return
                    result.then(    //为了将result状态变更成resolved
                        (val) =>{resolve(val)},
                        (err) =>{reject(err)}
                    )
                    return result
                }else{
                    resolve(result);
                }
            })
        }else{
            setTimeout(() => {
                onReject(self.data)
            })
        }
    }
}

写到这里,一个简单的手写 Promise 就算是实现了,如下是完整代码:

(function(window){
    MyPromise.prototype.then = function(onResolve,onReject){
        let self = this;
        return new MyPromise((resolve,reject) =>{
            if(self.status === 'pending'){
                self.callbacks.push({
                    onResolve,
                    onReject
                })
            }else if(self.status === 'resolved'){
                setTimeout(() => {
                    const result = onResolve(self.data)
                    if(result instanceof MyPromise){
                        result.then( 
                            (val) =>{resolve(val)},
                            (err) =>{reject(err)}
                        )
                        return result
                    }else{
                        resolve(result);
                    }
                })
            }else{
                setTimeout(() => {
                    onReject(self.data)
                })
            }
        }
    }
    MyPromise.prototype.catch = function(onReject){
        
    }
    
    function MyPromise(executor){
        let self = this 
        self.status = 'pending' 
        self.data = undefined 
        self.callbacks = []
    
        function resolve(value){
            if(self.status !== 'pending'){
                return
            }
            self.status = 'resolved'
            self.data = value     
            if(self.callbacks.length>0){
                setTimeout(()=>{
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onResolve(self.data)
                    })
                })
            }
        }
        function reject(){
            if(self.status !== 'pending'){
                return 
            }
            self.status = 'rejected'
            self.data = value
            if(self.callbacks.length>0){
                setTimeout(()=>{
                    self.callbacks.forEach(callbackObj => {
                        callbackObj.onReject(self.data) 
                    })
                })
            }
        }
        
        executor(resolve,reject)
    }
    
    window.MyPromise = MyPromise    
})(window)

那么我们来看看效果吧:

image.png

可以看到已经完成一个简单的手写 Promise.then 了,这篇文章也就到此为止啦,看完不妨点个小赞吧。