• 大厂面试题手写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源码就全部已经完成。阅读源码对我们思维能力会有很大的提升的,希望大家不要都可以动手实现一下,相信会有不少收获。


































    不积跬步无以至千里
  • 相关阅读:
    【计算机视觉】OpenCV篇(6)
    【计算机视觉】OpenCV篇(5)
    C++解决头文件相互包含问题的方法
    【计算机视觉】OpenCV篇(4)
    java mysql多次事务 模拟依据汇率转账,并存储转账信息 分层完成 dao层 service 层 client层 连接池使用C3p0 写入库使用DBUtils
    MySQL事务隔离级别 解决并发问题
    在jdbc基础上进阶一小步的C3p0 连接池(DBCP 不能读xml配置文件,已淘汰) 和DBUtils 中两个主要类QueryRunner和ResultSetHandler的使用
    java数据库 JDBC操作MySQL数据库常用API 部门表和员工表 创建表 添加数据 查询数据
    java XML 通过BeanUtils的population为对象赋值 根据用户选择进行dom4j解析
    java基础 xml 使用dom4j解析 xml文件 servlet根据pattern 找到class
  • 原文地址:https://www.cnblogs.com/lyt0207/p/12387564.html
Copyright © 2020-2023  润新知