大厂面试题手写Promise源码

手写Promise源码几乎是每个大厂面试要求必会的一个考点,每次听到源码,总有一种让人上头的感觉,因为自己从来没有实现过,总感觉这东西很难实现,最近再为跳槽做准备,从头写学了一下基础知识,手写了call源码、apply源码、Promise源码,感觉还挺有意思,不是想想的那么难。就是一个js的简答实现。只要优点js基础的人都能手写出来,所以不要一听“源码”二字就给吓到。自己动手实现一遍,比看别人的几十遍的效果更好。

本篇文章从实际应用角度思考Promise是怎样的一个实现过程,会先从简单的应用出发,然后一点一点去完善整个代码。

先来一个简单的例子:

//刚开始是等待态,pending
let promise = new Promise( (resolve,reject) =>{if(err) return reject(err) //失败了返回失败信息 失败态
        resolve(data) //成功了返回数据 成功态
})
//状态改变了调用
promise.then(data=>{ //成功了调用
    console.log(data)
},err=>{             //失败了调用
    console.log(err)
})

这是一个promise实例,它有三种状态,pending(等待态)、fullfilled(成功态)、rejected(失败态),resolve为成功时调用,状态由等待态变为成功态,reject为失败,状态由等待态变为失败态。

当状态改变的了的时候会执行then方法,如果是成功就输出值成功的值,如果是失败就返回失败的原因。但是他们两个只能调用一个,不可能有即成功由失败的情况。

根据这个实例我们就可以实现一版简单的功能。

第一版:定义整个流程,实现同步功能:

这一版我们主要实现流程能走通,能执行同步任务

class MyPromise{
    constructor(executorCallback){ //executorCallback执行的回调函数
        let _this = this;
        _this.status = 'pending'; //记录状态
        _this.value = ''; //记录返回的数据
        function resolveFn(result){
            if(_this.status === 'pending'){ //状态只能是从pending到成功
                _this.status = 'resolved'
                _this.value = result
            }
        }
        function rejectFn(reason){
            if(_this.status === 'pending'){ //状态只能是从pending到失败
                _this.status = 'rejected'
                _this.value= reason
            }
        }
     executorCallback(resolveFn,rejectFn)
    }
    //判断状态成功还是失败,成功了执行onFullfiled,失败了执行onRejected
    then(onFullfiled,onRejected) {
        if(this.status === 'resolved'){
            onFullfiled(this.data)
        }
        if(this.status === 'rejected'){
            onRejected(this.err)
        }
    }
}
1.定义_this保证每次都能获取到正确的this
2.定义一个常量status用来记录promise的状态
3.定义一个常量value用来保存执行结果的返回值
4.executorCallback是立即执行函数,需要我们手动传入两个参数,这两个参数代表成功和失败时候调用,需要我们在内部定义好。
5.resolveFn()和rejectedFn()函数,在执行前先判断一下状态,如果状态为pending就执行,并且让状态变为相应的成功态或者失败态,这样每次就只能执行一个,要么是resolveFn(),
要么是rejectedFn(),并且把相应的返回值赋值给value。
6.状态改变了之后就会调用Promise.then()方法,如果状态是成功态就执行onFullfilled(),如果状态是失败态,就执行onRejected()
到此基本流程通了,我们写个小案例测一下
可以看到,虽然我写了两个函数,但是它只执行了第一个,就说明当状态pending变为成功态之后不会再去执行失败的函数,同理当状态变为失败态之后也不会再去执行成功的函数,
现在虽然实现了可以执行同步任务,但是对于异步任务还是执行不了。例如

 可以看到控制台没有输出任何东西,现在我们就来解决一下如何实现异步任务。

第二版:实现异步功能

我们先来分析一下上一版为什么实现不了异步功能,当代码执行到setTimeout时,不会立即执行里面的函数,而是先放到一个异步调用栈里面,等到同步代码执行完了在执行里面的resolveFn函数,这个时候then已经执行完了,不会再执行,所以就不会输出任何东西,我们可以在resolveFn执行之前就将then的回调函数先保存起来,等到resolveFn执行的时候再去一个一个执行这些回调函数,这个时候就可以实现异步功能。

在原来的基础上修改

class myPromise{
    constructor(executorCallback){
        var _this = this
        _this.status = 'pending'
        _this.value 
        _this.onFullfilledCallback  = [] //存放成功时的回调函数
        _this.onRejectedCallback = [] //存放时的失败的回调函数
        function resolveFn (result) {
            let timer = setTimeout( () =>{ //异步任务
                clearTimeout(timer)
                // console.log('chengg')
                if(_this.status === 'pending'){
                    _this.status = 'resolved'
                    _this.value = result
                    _this.onFullfilledCallback .forEach(item =>item(_this.value));
                }
            })
        }
        function rejectFn (err) {
            let timer = setTimeout( () =>{ //异步任务
                clearTimeout(timer)
                if(_this.pending === 'pending'){
                    _this.status = 'rejected'
                    _this.value = err
                    _this.onRejectedCallback.forEach(item =>item(_this.value))
                }
            })
        }
        executorCallback(resolveFn,rejectFn)
    }
    
    then(onFullfiled,onRejected){
        if(this.status === 'pending') {
            this.onFullfilledCallback .push(onFullfiled)
            this.onRejectedCallback.push(onRejected)
        }
    }
}

我将修改了的部分用红色字体标出。

1.onResolvedCallback和onRejectedCallback用来存放成功和失败时候的回调函数,想想为什么是一个数组呢?我们前面分析过,第一个原因是同一个Promise实例可以调用多次then,
需要把这些方法都放在同一个数组里,例如
let p1 = new Promise( (resolve,reject) =>{
    setTimeout(function(){
        resolve('ok')
    },1000)
})
p1.then(result =>{
    console.log('result1:'+result)
},reason =>{
    console.log(reason)
})
p1.then(result =>{
    console.log('result2:'+result)
},reason =>{
    console.log(reason)
})

 第二个原因是当立即执行完 Promise 时,让它的状态还是pending的时候,应该把 then 中的回调保存起来,当执行成功或者失败,状态改变时再执行

 
 

2.resolveFn()和rejected()这里为什么要用setTimeout将它变为异步执行呢?因为如果不用setTimeou这种方式的话,若Promise里面的代码是同步代码,在执行到reject或者resolve的时候,还没有执行then,所以数组里还没有值,这个时候调用的话不会报错但是不会输出任何结果,用setTimeout转为异步的话,会先去执行then方法,将回调收集到数组里,然后再去执行异步任务,这个时候就有值了。举例子:

 此时红色方框内的是同步代码,会先执行,不会输出任何东西,当然如果把红色方框内的变为异步代码就不会有这个问题了。但是我们要同时兼顾同步和异步都存在的情况。

3.then方法:

then(onFullfiled,onRejected){
        if(this.status === 'pending') {
            this.onFullfilledCallback .push(onFullfiled)
            this.onRejectedCallback.push(onRejected)
        }
    }

then很简单,就是在状态为pending的时候将回调函数收集到数组里面,到此异步功能就差不多了,我们写个例子试一下。

let p1 = new myPromise((resolve,reject) =>{
    setTimeout(function(){
        resolve('第一次成功')
    },1000)
})
p1.then(result =>{
    console.log('result:'+result)
},reason =>{
    console.log('reason:'+reason) 
})

 完美。

但是这个第二版还不能实现链式调用,在工作中我们经常通过promis.then().then()这样的方式来解决回调地狱,如下图。

 接下来我们就实现链式调用

第三版:实现链式调用

首先分析,Promise为什么可以实现链式调用,因为Promise.then()方法它返回的是一个新的Promise实例,将这个新的Promise实例的返回值传递到下一个then中,作为下次onFullfilled()或者onRejected()的值。那这个新的Promise的返回值会有哪几种情况呢?我们来分析一下

 

1.如果返回的是一个普通值就直接执行成功,resolve(x)
2.如果返回的是一个promise实例,就继续new
3.如果出错了,就执行失败reject(x)
4.如果参数是null,就会输出undefined
所以要对返回值进行解析。
改写then方法。
then(onFulfiled,onRejected){
        // 声明返回的promise2     
        let promise2 = new myPromise((resolveFn, rejectFn)=>{       
            if (this.status === 'fulfilled') {         
                let x = onFulfiled(this.value);         
                // resolvePromise函数,处理自己return的promise和默认的promise2的关系         
                resolvePromise(promise2, x, resolveFn, rejectFn);       
            };       
            if (this.status === 'rejected') {         
                let x = onRejected(this.reason);         
                resolvePromise(promise2, x, resolveFn, rejectFn);       
            };       
            if (this.status === 'pending') {         
                this.onFullfilledCallback.push(()=>{           
                    let x = onFulfiled(this.value);           
                    resolvePromise(promise2, x, resolveFn, rejectFn);         
                })         
                this.onRejectedCallback.push(()=>{           
                    let x = onRejected(this.reason);           
                    resolvePromise(promise2, x, resolveFn, rejectFn);         
                })       
            }
        });     
        // 返回promise,完成链式     
        return promise2;   
    }

我们首先定义一个新的Promise实例,在这个实例内部需要判断状态,当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数。将then回调函数的返回值记为x,由于这个返回值可能有多种情况,所以需要对各种情况进行解析。所以另外封装一个方法resolvePromise(),接下来我们就来封装一下这个函数。

function resolvePromise(promise2, x, resolve, reject){   
                // 循环引用报错   
                if(x === promise2){     
                    // reject报错     
                    return reject(new TypeError('Chaining cycle detected for promise'));    
                }     
                let called;   //控制调用次数
                // x不是null 且x是对象或者函数   
                if (x !== null && (typeof x === 'object' || typeof x === 'function')) {     
                    try {       
                        // PromiseA+规定,声明then 等于 x的then方法       
                        let then = x.then;       
                        // 如果then是函数,就默认是promise了       
                        if (typeof then === 'function') {          
                            // 就让then执行 第一个参数是this,   后面是成功的回调 和 失败的回调         
                            then.call(x, y => {           
                                // 成功和失败只能调用一个           
                                if (called) return;           
                                called = true;           
                                // resolve的结果依旧是promise 那就继续解析           
                                resolvePromise(promise2, y, resolve, reject);         
                            }, err => {           
                                // 成功和失败只能调用一个           
                                if (called) return;           
                                called = true;           
                                reject(err);     
                            })       
                        } else { //如果不是函数,是普通对象直接resolve
                            resolve(x); // 直接成功即可       
                        }     
                    } catch (e) {       
                        // 如果在执行的过程中报错了,就被then的失败捕获       
                        if (called) return;       
                        called = true;       // 取then出错了那就不要在继续执行了       
                        reject(e);      
                    }   
                } else { //如果是普通值     
                    resolve(x);   
                } 
            }
1.PromiseA+规定 x 不能与 promise2 相等,这样会发生循环引用的问题
2.定义一个called来控制调用次数,成功和失败只能调用一个,一旦调用完就将called = true,防止下一个再调用。
3.接着判断x的类型,如果不是对象或者函数,那就是普通值,直接resolve(x)
4.如果是对象或者函数,将x.then赋值给then,如果then是函数,就让then执行回调函数。如果下一次的执行结果还是一个Promise,就接着处理。如果then是个普通对象,就直接执行
resolve方法
5.再执行这段代码的过程中可能会发生异常,我们用try catch去捕获错误,如有错误,就直接执行reject方法。
来测试一下是否可以实现链式调用了:

 没毛病。

到此,Promise的核心功能都已经完成了,Promise还有一些其他的方法,all、race、resolve、reject,相信理解了上面的封装流程,大概就知道怎么封装了,当然前提是要知道这些方法的用法。接下来就看一下这些方法的实现。

Promise其他方法的实现

简单说一下,all方法接收一个数组,等到数组里面的所有Promise实例的状态都变成成功态之后才成功,但只要有一个失败了就返回失败。race方法是哪个先成功就先返回哪个,resolve和reject是分别执行成功和是失败。代码如下

//all 获取所有的promise,都执行then,把结果放到数组,一起返回
    static all(promiseArr =[]) {
        return new Promise((resolve,reject) =>{
            let index = 0;
            let arr = []
            for(let i =0; i<promiseArr.length; i++){
                promiseArr[i].then(result =>{
                    index++
                    arr[i] = result
                    if(index === arr.length){
                        resolve(arr)
                    }
                },reason =>{
                    reject(reason)
                })
            }
        })
    }
    //谁先执行先返回谁
    static race (promises){   
        return new Promise((resolve,reject)=>{     
            for(let i=0;i<promises.length;i++){       
                promises[i].then(resolve,reject)     
            };   
        }) 
    }
    //resolve方法 
    static resolve(result){   
        return new Promise((resolve,reject)=>{     
            resolve(result)   
        }); 
    } 
    //reject方法 
    static reject (reason){   
        return new Promise((resolve,reject)=>{     
            reject(reason)   
        }); 
    } 

测试:

          

 Promise源码就全部已经完成。阅读源码对我们思维能力会有很大的提升的,希望大家不要都可以动手实现一下,相信会有不少收获。


































猜你喜欢

转载自www.cnblogs.com/lyt0207/p/12387564.html