前端面试题之Promise问题

前言

在我们日常开发中会遇到很多异步的情况,比如涉及到 网络请求(ajax,axios等)定时器这些,对于这些异步操作我们如果需要拿到他们操作后的结果,就需要使用到回调函数。拿请求来说,如果我们需要拿到请求回来的数据我们就需要利用回调函数(见代码片段1),以下所有的请求都是使用jQuery的ajax模拟。

点击查看代码片段1
    // 代码片段1
    $.ajax({
        url: 'url',
        type: 'post',
        data: {
            参数1: 值1,
            参数2: 值2
        },
        success: function(res){
            // success就是响应成功的回调函数,在此处可以获取到响应返回的内容
            console.log(res)
        }
    })

在网络请求中会遇到请求之前的依赖,一个请求会依赖于另一个请求的时候,就会需要出现回调嵌套的问题(代码片段2)

点击查看代码片段2
    // 代码片段2
    // 请求2的请求参数是请求1的响应结果
    // 请求3的请求参数是请求2的响应结果
    $.ajax({
        url: '请求1的地址',
        type: 'post',
        data: {
            参数1: 值1,
            参数2: 值2
        },
        success: function(res){
            // res是请求1的响应结果
            console.log(res)
            $.ajax({
                url: '请求2的地址',
                type: 'post',
                data: {
                    // 参数1是请求1的响应结果
                    参数1: res,
                    参数2: 值2
                },
                success: function(res2){
                    // res2是请求2的响应结果
                    $.ajax({
                        url: '请求3的地址',
                        type: 'post',
                        data: {
                            // 参数1是请求2的响应结果
                            参数1: res2,
                            参数2: 值2
                        },
                        success: function(res3){
                            // res3是最终请求的结果
                            console.log(res3)
                        }
                    })
                }
            })
        }
    })

这样的代码就出现了js编程里面的著名回调地狱问题,为了解决这个问题我们需要利用es2015的promise和es2016的await来解决(代码片段3)

点击查看代码片段3
    // 代码片段3
    // await不能写在同步的代码里面,会阻塞整个程序的进程,只能写在异步的代码里面
    // async可以修饰一个函数让这个函数变成一个异步的函数
    async function getRes(){
        // $.ajax() 返回的是一个promise对象
        // await可以等待一个promise状态结束,拿到响应的结果
        const res1 = await $.ajax({
            url: '请求1的地址',
            type: 'post',
            data: {
                参数1: 值1,
                参数2: 值2
            }
        })
        const res2 = await $.ajax({
            url: '请求2的地址',
            type: 'post',
            data: {
                参数1: res,
                参数2: 值2
            }
        })
        const res3 = await $.ajax({
            url: '请求3的地址',
            type: 'post',
            data: {
                参数1: res2,
                参数2: 值2
            }
        })
    }
    // 调用异步函数
    getRes()

利用promise和await就可以将之前的回调嵌套地狱改成同步的代码方式,但是这里面大家要注意await的使用事项,await不允许出现在同步代码块里面,会阻塞同步代码执行,必须写在异步的代码块里面,加async让getRes函数成为一个异步的函数,这样getRes执行不会阻塞全局,getRes函数内部代码就是一个同步的方式执行,就解决了回调嵌套的问题。本来故事到这里就该说goodBye了,但是现在很多面试,尤其是一些大厂的面试要求我们自己实现promise和await,这章节先给大家实现promise。

Promise的封装

1. promise的介绍和使用

  • 状态(state)

    • pending
      • promise的初始状态,并且此状态可以转换成fulfilledrejected
    • fulfilled
      • promise的成功状态,不可以转换其他状态,并且必须有一个不可改变的最终值value
    • rejected
      • promise的失败状态,不可以转换其他状态,并且必须有一个不可改变的原因reason
  • 术语

    • 解决(fulfill)

      当调用resolve方法时promise的状态变成fulfill

    • 拒绝(reject)

      当调用reject方法时promise的状态变成reject

    • 终值(value)

      所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。

    • 拒因(reason)

      也就是拒绝(失败)的原因,指在 promise 被拒绝时传递给拒绝回调的值。

  • 面向对象编程方式

    • Promise是面向对象编程方式,对应的构造函数是 Promise
    • 使用Promise的时候需要创建一个实例对象,使用实例对象的方法
    • Promise构造函数实例化的时候需要传递一个函数(handler),此函数里面可以接收两个参数,一个函数叫做resolve,一个函数叫做reject
    • 调用resolve时候promise实例的状态变为 fulfilled
    • 调用reject时候promise实例的状态变为 rejected
    • 此处要注意一个promise实例只有一个状态,也就是不能同时调用resolve和reject
点击查看代码
    // promise01就是实例化Promise构造函数得到的实例对象
    const promise01 = new Promise((resolve,reject)=>{
        // 此处调用resolve时promise状态会变成fulfilled
        // 此处调用reject时promise状态会变成rejected
    })
  • 实例对象的方法
    • then方法
      • 该方法有两个参数,可选,分别对应onFulfilled,onRejected
      • 当状态为fulfilled时,调用onFulfilled,得到一个终值 value
      • 当状态为rejected时,调用onRejected,得到一个拒因 reason
    • catch方法
      • 该方法有一个参数,可选,对应onRejected
      • 当状态为rejected时,调用onRejected,得到一个拒因
    • finally方法
      • 该方法有一个参数,可选
      • 该方法无论是promise状态成功还是失败都会调用
    • 注意:
      1. 实例方法可以实现链式调用,因为每一次方法调用完成之后都会返回一个promise实例
      2. then方法可以调用多次
点击查看代码
    const promise01 = new Promise((resolve,reject)=>{
    })
    // 使用实例对象方法 链式调用
    promise01.then(
        // onFulfilled
        function(value){
            // 当promise实例的状态为fulfilled调用
        },
        // onRejected
        function(reason){
            // 当promise实例的状态为rejected调用
        }  
    ).then(
        // then方法可以调用多次
        // 并且调用可以不传递参数
    ).catch(function(reason){
            // 当promise实例的状态为rejected调用
    }).finally(function(){
        // 无论promise实例的状态为fulfilled或者rejected都会调用
    })

2. 实现手写Promise

  1. 实现Promise的构造函数

    • 构造函数需要参数,参数必须是一个函数类型 这里面我们叫做handler,这儿会对handler类型进行验证,如果不是函数类型会抛出类型异常
    • handler函数需要两个形式参数 对应的是类自身的两个方法
      • resolve方法
        • 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
        • 调用方法promise的状态变为fulfilled
        • 调用该方法会得到一个终值value
        • 该方法可能异步调用
        • 该方法里面会用到实例的this,在调用的时候需要改变this的指向
      • reject方法
        • 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
        • 调用方法promise的状态变为rejected
        • 调用该方法会得到一个拒因reason
        • 该方法可能异步调用
        • 该方法里面会用到实例的this,在调用的时候需要改变this的指向
    • 实例里面属性
      • value 终值
      • reason 拒因
      • state 状态
        • 状态可以作为静态的属性,也可以作为实例的属性
        • 状态会多次用于判断,所以建议用常量保存
    • 实例里面的方法
      • then
      • catch
      • finally
    • 类自身的方法
      • resolve
      • reject
        // promise.js
        // 状态会多次用于判断,所以建议用常量保存
        const PENDING = 'pending'
        const FULFILLED = 'fulfilled'
        const REJECTED = 'rejected'
        class Promise{
            constructor(handler){
                // 构造函数需要参数,参数必须是一个函数类型 这里面我们叫做handler,这儿会对handler类型进行验证,如果不是函数类型会抛出类型异常
                if(typeof handler !== 'function'){
                    throw new TypeError(`Promise构造函数的参数${handler}不是一个函数`)
                }
                this.state = PENDING // 用来存储promise的状态 初始化状态为pending
                this.reason = undefined // 拒因
                this.value = undefined // 终值
                // resolve,reject方法里面会用到实例的this,在调用的时候需要改变this的指向
                handler(this.resolve.bind(this), this.reject.bind(this))
            }
            resolve(value){
                // 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
                if(this.state !== PENDING) return
                // 调用方法promise的状态变为fulfilled
                this.state = FULFILLED
                // 调用该方法会得到一个终值value
                this.value = value
            }
            reject(reason){
                // 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
                if(this.state !== PENDING) return
                // 调用方法promise的状态变为rejected 
                this.state = REJECTED
                // 调用该方法会得到一个拒因reason
                this.reason = reason
            }
        } 
    
        // 试错
        const p = new Promise(1)
        console.log(p)
        // promise.js:9 Uncaught TypeError: Promise构造函数的参数1不是一个函数
        // 正确操作
        // 状态成功
        const p = new Promise((resolve, reject) => {
            resolve()
        })
        console.log(p)
        // Promise {state: 'fulfilled', reason: undefined, value: undefined}
        // 状态失败
        const p = new Promise((resolve, reject) => {
            reject()
        })
        console.log(p)
        // Promise {state: 'rejected', reason: undefined, value: undefined}
    
  2. 实现实例then方法

  • then方法接受两个参数,两个参数类型是函数

    • onFulfilled
      • 需要对onFulfilled类型进行验证,如果不是一个函数就设置为默认函数function(value){return value},这个函数是获取终值
      • 该函数在promise状态为fulfilled时候调用,传入终值
    • onRejected
      • 需要对onRejected类型进行验证,如果不是一个函数就设置为一个默认函数function(err){throw err},这个函数是获取拒因
      • 该函数在promise状态为rejected时候调用,传入拒因
        // promise.js
            const PENDING = 'pending'
            const FULFILLED = 'fulfilled'
            const REJECTED = 'rejected'
            class Promise{
                constructor(handler){...}
                resolve(value){...}
                reject(reason){...}
                then(onFulfilled, onRejected){
                    // 参数可选要进行判断 如果传递不是一个函数 默认一个函数
                    onFulfilled = typeof onFulfilled==='function' ? onFulfilled : value => value;
                    onRejected = typeof onRejected==='function' ? onRejected : err => {throw err};
                    if(this.state === FULFILLED){
                        // 当promise状态为fulfilled时候调用onFulfilled
                        onFulfilled(this.value)
                    }
                    if(this.state === REJECTED){
                        // 当promise状态为rejected时候调用onRejected
                        onRejected(this.reason)
                    }
                }
            }
    
        // test.js
            // 成功状态
            const p = new Promise((resolve, reject) => {
                resolve('success')
            })
            p.then(
                res => {
                    console.log('调用了onFullfilled')
                    console.log(res)
                },
                err => {
                    console.log('调用了onRejected')
                    console.log(err)
                }
            )
            // 控制台输出 调用了onFullfilled sucess
    
    
            // 失败状态
            const p = new Promise((resolve, reject) => {
                reject('error')
            })
            p.then(
                res => {
                    console.log(res)
                },
                err => {
                    console.log(err)
                }
            )
            // 控制台输出 调用了onRejected error
    
    
            // handler函数异步调用resolve
            const p = new Promise((resolve, reject) => {
                setTimeout(()=>{
                    resolve('success')
                },3000)
            })
            p.then(
                res => {
                    console.log('调用了onFullfilled')
                    console.log(res)
                },
                err => {
                    console.log('调用了onRejected')
                    console.log(err)
                }
            )
            // 控制台没有任何内容输出 onFulfilled函数并没有被调用
    
    • 此时经过测试,目前的promise可以正常的调用,此时针对于同步的调用没有问题,但是在handler里面 resolvereject 大部分情况下都是异步调用的,比如说延迟3s调用,此时then里面的onFulfilledonRejected是无法执行的,因为then函数在执行的时候promise的状态是pending,onFulfilledonRejected函数里面是做了判断,没有对pendng状态进行处理,针对于pending状态的需要处理
        // promise.js
        // ...
        class Promise {
            // 用来存储状态成功的回调函数
            // 用来存储状态失败的回调函数
            successCallBack = null
            errorCallBack = null
            constructor(handler) {...}
            resolve(value) {
                // ...
                // 异步调用的时候执行存储的onFulfilled函数,传入终值
                this.successCallBack(this.value)
    
            }
            reject(reason) {
                // ...
                // 异步调用的时候执行存储的onRejected函数,传入拒因
                this.errorCallBack(this.reason)
            }
            then(onFulfilled, onRejected) {
                // ...
                if (this.state === PENDING) {
                    // 当resolve或者reject异步调用,then执行的时候promise状态等待
                    // 将onFulfilled和onReject函数存储起来
                    this.successCallBack = onFulfilled
                    this.errorCallBack = onRejected
                }
            }
        }
    
        // test.js
        // 异步调用resolve
        const p = new Promise((resolve, reject) => {
            setTimeout(()=>{
                resolve('success')
            },3000)
        })
        p.then(
            res => {
                console.log('调用了onFullfilled')
                console.log(res)
            },
            err => {
                console.log('调用了onRejected')
                console.log(err)
            }
        )
        // 3s后控制台输出 调用了onFullfilled  success
    
    • 此时promise的handler就可以异步调用 resolvereject,then里面可以获取到终值或据因
    • then函数支持多次调用,所以此时then的回调函数就不止一个,我们将之前的变量改成数组用来存储then的回调函数
        // promise.js
        // ...
        class Promise {
            // 用数组的方式存储失败和成功的回调函数
            successCallBack = []
            errorCallBack = []
            constructor(handler) {...}
            resolve(value) {
                // ...
                // 遍历数组将所有成功的函数执行
                this.successCallBack.forEach(fn=> fn())
            }
            reject(reason) {
                // ...
                // 遍历数组将所有失败的函数执行
                this.errorCallBack.forEach(fn=>fn())
            }
            then(onFulfilled, onRejected) {
                // ...
                if (this.state === PENDING) {
                    // 当resolve或者reject异步调用,then执行的时候promise状态等待
                    // 将onFulfilled和onReject函数存储起来
                    this.successCallBack.push(()=>{
                        onFulfilled(this.value)
                    }) 
                    this.errorCallBack.push(()=>{
                        onRejected(this.reason)
                    })
                }
            }
        }
    
    • 测试调用了两次then,状态成功之后两次then的成功回调都会被执行
        // test.js
        // 异步调用resolve
        const p = new Promise((resolve, reject) => {
            setTimeout(()=>{
                resolve('success')
            },3000)
        })
        p.then(
            res => {
                console.log('调用了onFullfilled')
                console.log(res)
            },
            err => {
                console.log('调用了onRejected')
                console.log(err)
            }
        )
         p.then(
            res => {
                console.log('调用了onFullfilled01')
                console.log(res)
            },
            err => {
                console.log('调用了onRejected01')
                console.log(err)
            }
        )
        // 3s后控制台输出 调用了onFullfilled  success  调用了onFullfilled01 success
    
    • 但是目前then不支持链式调用,需要进行链式调用的支持
    • 支持链式调用的条件
      1. 只有then函数调用返回一个promise对象,一个新的promise对象newPromise
      2. then函数有一个返回值 这个返回值result就是新promise的onFulfill或onRejected的值
    • 接下来我们需要判断这个返回值result的类型
      1. 一个具体值
      2. 一个新的promise
    • 另外我们需要一个函数去处理这种链式调用的问题handlerChainPromise,这个函数接收参数如下
      1. result 老的promise的then返回值,作为新promise的onFulfill或onRejected
      2. resolve成功处理函数
      3. reject失败处理函数
    class Promise {
        then(onFulfilled, onRejected) {
            // 每一次调用返回一个promise
            return new Promise((resolve,reject) => {
                // 参数可选要进行判断 如果传递不是一个函数 默认一个函数
                onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
                onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
                if (this.state === PENDING) {
                    // 当resolve或者reject异步调用,then执行的时候promise状态等待
                    // 将onFulfilled和onReject函数存储起来
                    this.successCallBack.push(() => {
                        let result = onFulfilled(this.value)
                        this.handlerChainPromise(result, resolve, reject)
                    })
                    this.errorCallBack.push(() => {
                        let result = onRejected(this.reason)
                        this.handlerChainPromise(result, resolve, reject)
                    })
                }
                if (this.state === FULFILLED) {
                    // 当promise状态为fulfilled时候调用onFulfilled
                    let result = onFulfilled(this.value)
                    this.handlerChainPromise(result, resolve, reject)
                }
                if (this.state === REJECTED) {
                    // 当promise状态为rejected时候调用onRejected
                    let result = onRejected(this.reason)
                    this.handlerChainPromise(result, resolve, reject)
                }
            })
        }
        handlerChainPromise(result,resolve,reject){
            // 如果返回的是一个promise就调用它的then方法
            // 如果返回的是一个具体值就直接返回值
            if(result instanceof Promise){
                result.then(resolve, reject)
            }else{
                resolve(result)
            }
        }
    }
    
    • 测试代码如下
        // 返回一个具体值(可以使任何类型)
        const p = new Promise((resolve, reject) => {
            setTimeout(()=>{
                resolve('success')
            },3000)
        })
        p.then(
            res => {
                return 11
            },
            err => {
                console.log(err)
            }
        ).then(
            res=>{
                console.log(res)
            }
        )
        // 返回一个promsie
        const p = new Promise((resolve, reject) => {
            setTimeout(()=>{
                resolve('success')
            },3000)
        })
        p.then(
            res => {
                return new Promise(resolve=>{
                    setTimeout(()=>{
                        resolve(22)
                    }, 1000)
                })
            },
            err => {
                console.log(err)
            }
        ).then(
            res=>{
                console.log(res)
            }
        )
    
    • 以上一个完整的promise就封装成功了
    • 完整代码如下
    // 状态会多次用于判断,所以建议用常量保存
    const PENDING = 'pending'
    const FULFILLED = 'fulfilled'
    const REJECTED = 'rejected'
    class Promise {
        // 用来存储状态成功的回调函数
        // 用来存储状态失败的回调函数
        successCallBack = []
        errorCallBack = []
        constructor(handler) {
            // 构造函数需要参数,参数必须是一个函数类型 这里面我们叫做handler,这儿会对handler类型进行验证,如果不是函数类型会抛出类型异常
            if (typeof handler !== 'function') {
                throw new TypeError(`Promise构造函数的参数${handler}不是一个函数`)
            }
            this.state = PENDING // 用来存储promise的状态 初始化状态为pending
            this.reason = undefined // 拒因
            this.value = undefined // 终值
            // resolve,reject方法里面会用到实例的this,在调用的时候需要改变this的指向
            handler(this.resolve.bind(this), this.reject.bind(this))
        }
        resolve(value) {
            // 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
            if (this.state !== PENDING) return
            // 调用方法promise的状态变为fulfilled
            this.state = FULFILLED
            // 调用该方法会得到一个终值value
            this.value = value
            // 异步调用的时候执行存储的onFulfilled函数,传入终值
            this.successCallBack.forEach(fn => fn())
        }
        reject(reason) {
            // 该方法只能在promise状态为pending的时候调用,如果promise已经有了状态,则不允许调用
            if (this.state !== PENDING) return
            // 调用方法promise的状态变为rejected 
            this.state = REJECTED
            // 调用该方法会得到一个拒因reason
            this.reason = reason
            // 异步调用的时候执行存储的onRejected函数,传入拒因
            this.errorCallBack.forEach(fn => fn())
        }
        then(onFulfilled, onRejected) {
            // 每一次调用返回一个promise
            return new Promise((resolve,reject) => {
                // 参数可选要进行判断 如果传递不是一个函数 默认一个函数
                onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
                onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
                if (this.state === PENDING) {
                    // 当resolve或者reject异步调用,then执行的时候promise状态等待
                    // 将onFulfilled和onReject函数存储起来
                    this.successCallBack.push(() => {
                        let result = onFulfilled(this.value)
                        this.handlerChainPromise(result, resolve, reject)
                    })
                    this.errorCallBack.push(() => {
                        let result = onRejected(this.reason)
                        this.handlerChainPromise(result, resolve, reject)
                    })
                }
                if (this.state === FULFILLED) {
                    // 当promise状态为fulfilled时候调用onFulfilled
                    let result = onFulfilled(this.value)
                    this.handlerChainPromise(result, resolve, reject)
                }
                if (this.state === REJECTED) {
                    // 当promise状态为rejected时候调用onRejected
                    let result = onRejected(this.reason)
                    this.handlerChainPromise(result, resolve, reject)
                }
            })
        }
        handlerChainPromise(result,resolve,reject){
            // 如果返回的是一个promise就调用它的then方法
            // 如果返回的是一个具体值就直接返回值
            if(result instanceof Promise){
                result.then(resolve, reject)
            }else{
                resolve(result)
            }
        }
    }