完全理解并实现Promise其实可以让你进步很多 (记录手写Promise过程)

8,072 阅读8分钟

在JS里面我们想要优雅进行异步操作,必须是要熟悉一个ES6里面非常非常重要的概念Promise,它所具备的链式调用,很好的缓解了传统回调写法所带来的回调地狱,也是理解生成器,async/await的基础。在最近几年前端的面试中对其的考察基本是不可避免的,本文的主要目的是记录一下我在实现一个Promise的一些过程和思考。

全部代码我已经上传github

从最简单的例子开始

这是一个Promise最简单的用法,代码创建了一个Promise对象,传入一个executor执行函数,在某个时刻它会按顺序执行它的参数reslove和reject,然后resolve和reject的参数会作为Promise对象then的参数。了解了这些我们可以总结一下:

  • 我们会需要两个变量来分别存储resolve和reject的参数
  • 因为resolve和reject不会同时处理,所以我们需要一个状态变量来记录Promise对象的一个状态
  • then方法需要传入两个方法,在Promise对象改变状态的时候调用

下面就来简单实现一下吧

  • 构造函数

  • then方法

  • 测试

看起来现在好像之前那个简单例子的代码可以在手写的这个函数里面执行了,但是现在又有一个问题了,我们前面写的函数都是同步的逻辑那么异步问题该怎么解决呢

这是为啥呢,因为setTimeout的关系resolve方法被放入了执行队列中的,而then的逻辑是同步的,那么在执行then方法的时候,this.status的状态还是在pending.那么如何解决呢,其实很简单,我们可以在then方法内加入status为pending的逻辑,把传入的函数先存到一个数组中,将它放到resolve或者reject方法中执行就行了,那么我们可以稍微改造一下like this

加入异步操作

  • 构造函数

  • then

  • 测试

嘻嘻没有任何意外,完美的输出意料之中的答案,那么接下来就是最重要的链式调用以及值穿透的实现了,链式调用可以说是整个实现过程中最难理解的地方,但是说难其实如果你经常解除递归的思想以及JS基础比较扎实的情况下也是非常简单的,如果一开始不理解也没关系,小菜鸡在给出代码的同时也会完整的解释一遍运行的过程,相信聪明的你多看两遍一定就会懂得啦!

分析一下先:

如果需要链式调用那么关键的关键就是xxx.then这一部分是一个Promise对象对吧,那么很简单,为了保证实现这个功能,我们需要的是在调用then方法时一定会返回一个新的Promise对象,这是这条链不会断的关键,然后链的问题搞定之后我们需要处理传值的问题,根据Promise的规则

  • 如果 then 的返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调中;
  • 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调中;
  • 如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成- 功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;
  • 如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;
  • 如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;

总的来说就是下一个then前面那一部分是一个Promise,然后它的resolve或者reject方法主要看它return的部分,我们要做的就是每次使用then返回一个Promise实例,然后处理其中的return的部分就行了。那么走着

链式调用

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

  • 构造函数

  • then 划重点

  • getResolveValue (处理return部分的方法)

  • 测试部分

输出: 
     lavie小师弟
     hello JS
     hello TS

理解过程

结合前面的解释然后看完这段代码之后可能很多小伙伴会有一种似懂非懂的感觉,ok那么我们就按照前面给出的测试代码(为了彻底理解我故意将测试代码写的比较绕,求各位别打我,多递归警告⚠)走一遍相信大家看了小菜鸡写的这个过程会觉得,其实Promsie也不过如此,再次提醒可能会比较绕但是大家耐心看完一定会理解这几个函数的作用的这点非常重要

new PromiseAll((resolve,_)=>{
    setTimeout(()=>{
    resolve('lavie小师弟')
    },0
    )
}) //p1
.then((data)=>{ console.log(data) 
        return new PromiseAll((resolve,_)=>{
            resolve(new PromiseAll((resolve,_)=>{
                resolve('hello JS')
            }))
        })
} ) //p2
.then(data=>{ console.log(data) 
       return new PromiseAll((resolve,_)=>{
        resolve('hello TS')
    }) } ) //p3
    .then(data=>console.log(data)) //p4
  1. 首先这段代码创建了一个PromiseAll实例p1然后里面的有一个异步的函数加入到执行队列中,status为Pending
  2. 第一个PromsieAll实例p1调用了then方法创建了一个新实例p2,因为p1的状态为Pending所以将传入的函数放进p1的onSuccessCallback数组中等待p1的solve执行时再调用
  3. p2调用then方法创建了p3,然后同样的p3的函数保存在p2的onSuccessCallback数组中,同时p3调用then方法创建了p4...

上述过程是在执行栈中执行的同步事件,所以会率先执行

  1. 执行完同步操作之后,那么回过头来执行p1里面的resolve('lavie小师弟') 那么p1的状态就被修改成Success了,value也变成了lavie小师弟,然后调用传进去的
value=>{
           try {
                 let result = onFulfilled(value) //此时会输出lavie小师弟
                  getResolveValue(promiseNext,result,resolve,reject) //这里的promsieNext为p2
                } catch (error) {
                    reject(error)
                }
            }

根据我们传进去的函数这里的result会等于下面这个嵌套的对象记为PC1

//PC1
new PromiseAll((resolve,_)=>{
            resolve(new PromiseAll((resolve,_)=>{
                resolve('hello JS')
            }))
        })

好的搞清楚promsieNext和result之后我们就去执行getResolveValue(promiseNext,result,resolve,reject) 函数里面的逻辑

首先 then = PC1.then 用call方法将this绑定到PC1上面

所以第一次递归 this.value为

new PromiseAll((resolve,_)=>{
                resolve('hello JS')
            }) 

然后继续调用getResolveValue(promiseNext,next,resolve,reject) 因为next是等于this.value的,所以此时next参数变成了PC2

//PC2
new PromiseAll((resolve,_)=>{
                resolve('hello JS')
            }) 

此时进行第二次递归 this.value很明显变成了hello JS PC3

因为此时PC3已经不是对象了所以直接调用resolve('hello JS')

我们回过头来看,这个resolve方法其实是上面创建p2的时候传进去的,所以此时p2的状态就从Pending变成了Success,this.value也变成了hello JS ,然后执行在创建p3是时候传到p2的onSuccessCallback数组里面的方法此时输出hello JS 然后继续执行p3的递归.....和前面大同小异我就不继续说了,,,这个递归啊讲起来就是这么绕,但是你只要一步步跟的话,还是非常简单的😹,其实这步理解起来非常难受,但是没关系坚持自己给自己说几遍那么你一定会理解的!

穿透

这点就很简单啦

类似

new PromiseAll((resolve,_)=>{
                resolve('hello JS')
            }).then.().then().then(data=>console.log(data))

这点在上面的then方法中其实已经实现了

 onFulfilled = typeof onFulfilled === 'function'  ? onFulfilled : data => data
 onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

如果没有穿函数进来的话直接把值传递下去就可以啦

到这里其实整个Promsie都已经完成了,但是我没有完全按照 Promise/A+ 规范来写,有兴趣的小伙伴可以自己去完善🐶

接下来简单记录一下Promise中其他的方法的一下实现吧,大家细品

其他方法

Promise.prototype.catch

PromiseAll.prototype.catch = function(err){
    return this.then(null,err)
}
new PromiseAll((resolve,reject)=>{
    reject('错误')
}).catch(data=>console.log(data))

Promise.resolve && Promise.reject


PromiseAll.resolve = function(value){
    return new PromiseAll((resolve,_)=>{
        resolve(value)
    })
}
PromiseAll.reject= function(reason){
    return new PromiseAll((_,reject)=>{
        reject(reason)
    })
}

Promise.all

PromiseAll.all = function(arr){
    if(!Array.isArray(arr)){
        throw new TypeError('请传入一个数组')
    }
    return new PromiseAll((resolve,reject)=>{
        try {
            let resultArr = []
            const length = arr.length
            for(let i = 0;i<length;i++){
                arr[i].then(data=>{
                    resultArr.push(data)
                    if(resultArr.length === length){
                        resolve(resultArr)
                    }
                },reject)
            }
        } catch (error) {
            reject(error)
        }
    })
}

const p1 = new PromiseAll((resolve,_)=>{
    setTimeout(()=>{
    resolve('cjzz')
    },1000)
})

const p2 = new PromiseAll((resolve,_)=>{
    resolve('cjz')
})
PromiseAll.all([p1,p2]).then(data=>console.log(data))// ['cjz','cjzz']

Promise.race

PromiseAll.race = function(arr){
    if(!Array.isArray(arr)){
        throw new TypeError('请传入一个数组')
    }
    return new PromiseAll((resolve,reject)=>{
        try {
            const length = arr.length
            for(let i = 0;i<length;i++){
                arr[i].then(resolve,reject)
            }
        } catch (error) {
            reject(error)
        }
    })
}
PromiseAll.race([p1,p2]).then(data=>console.log(data,'race'))

如果有任何疑问欢迎留言鸭!