前端高频面试题之手写Promise

1、什么是 promise?

Promise 译为 "承诺",是 JavaScript 中用于处理异步操作的解决方案。它代表一个异步操作的最终完成(或失败)及其结果值。

2、Promise 的特点

特点一:Promise 有三种状态。

  • pending:等待态,promise的初始状态
  • fulfilled:成功态,promise调用resolve函数后即会从pending等待态变为fulfilled成功态
  • rejected:失败态:promise调用reject函数后即会从pending等待态变为rejected失败态

特点二:Promise 状态不可逆。

  • promise的状态一旦发生变更,便无法再更改。比如调用resolvepending变为fulfilled,它的状态就永远是fulfilled了,再调用reject也无法从fulfilled变成rejected
  • 并且状态只能从pending 变为fulfilledrejected,不能从fulfilledrejected返回到pending,这个也很好理解,状态只能前进不能倒退。

特点三:支持链式调用。

  • 通过 .then().catch().finally() 等方法进行链式调用。

3、Promise 手写流程

先看用法:

js 复制代码
const p = new Promise((resolve, reject) => {
    resolve(111);
})
p.then((value) => {
    console.log(value)
}, (error) => {
    console.log(error)
})

首先,Promise肯定是一个类,所以我们才可以new它,然后Promise实例化的时候给它传入一个回调我们叫它executor方法,Promise内部会立即调用这个executor方法,并且会传入resolvereject两个函数作为调用参数,另外在Promise类的原型上应该提供一个then方法,它里面可以传入两个回调,分别为Promise成功的回调Promise失败的回调。调用resolve后会走入成功的回调中,调用reject后会走入失败的回调中

3.1 版本一:搭建整体架子

js 复制代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class Promise {
    constructor(executor) {
        this.value = undefined
        this.reason = undefined
        this.status = PENDING
        
        const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
            }
        }
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
            }
        }
        executor(resolve, reject);
    }
    then(onFulfilled, onRejected) {
       if (this.status === FULFILLED) {
        onFulfilled && onFulfilled(this.value)
       }
       if (this.status === REJECTED) {
        onRejected && onRejected(this.reason)
       }
    }
}

module.exports = Promise;

3.2 版本二:支持异步的 resolve 或者 reject

如果是异步调用 resolve 或者 reject ,那么上面 onFulfilledonRejected 将无法执行了。

js 复制代码
const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(111);
    }, 1000)
})
p.then((value) => {
    console.log(value)
}, (error) => {
    console.log(error)
})

所以我们需用两个数组充当队列把 then 里边的回调存起来。

js 复制代码
class Promise {
    constructor(executor) {
        // ...
        // 定义两个数组
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];
        const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }
        // 默认执行executor函数,并传入resolve和reject函数
        executor(resolve, reject)
    }
    then(onFulfilled, onRejected) {
       if (this.status === FULFILLED) {
        onFulfilled && onFulfilled(this.value)
       }
       if (this.status === REJECTED) {
        onRejected && onRejected(this.reason)
       }
       if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
            onFulfilled(this.value)
        })
        this.onRejectedCallbacks.push(() => {
            onRejected(this.reason)
        })
       }
    }
}

这里定义了两个数组onResolvedCallbacksonRejectedCallbacks分别存储 then 里面成功的回调失败的回调,然后再调用resolvereject时分别循环执行这两个数组里存储的回调函数。

3.3 版本三:支持 Promise 链式调用

比如:下面这段代码:

js 复制代码
const p = new Promise((resolve, reject) => {
   setTimeout(() => {
    resolve(111)
   }, 1000)
})
p.then((value1) => {
    console.log('value1', value1)
    return 222
}, (error1) => {
    console.log('error1', error1)
}).then((value2) => {
    console.log('value2', value2)

}, (error2) => {
    console.log('error2', error2)
})

它的打印结果为:

这个是如何实现的呢?

这个其实也简单,它内部调用then方法时,返回了一个新的promise,并让这个新的promise接管了它下一个then方法。

注意:这里不能返回this,这样会导致多个then方法全部受同一个promise控制。

js 复制代码
class Promise {
    // ...
    then(onFulfilled, onRejected) {
       const promise2 = new Promise((resolve, reject) => {
        if (this.status === FULFILLED) {
            // onFulfilled方法可能返回值或者promise
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
           }
           if (this.status === REJECTED) {
            // onRejected方法可能返回值或者promise
            const x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
           }
           if (this.status === PENDING) {
            this.onResolvedCallbacks.push(() => {
                const x = onFulfilled(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
            this.onRejectedCallbacks.push(() => {
                const x = onRejected(this.reason)
                resolvePromise(promise2, x, resolve, reject)
            })
           }
       })
       return promise2
    }
}

最核心的就是resolvePromise,来看下它做了什么:

js 复制代码
function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>'))
    }
    let called
    // 判断x的类型 x是对象或函数才有可能是一个promise
    if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            const then = x.then
            if (typeof then === 'function') {
                // 只能认为它是一个promise
                then.call(x, (y) => {
                    if (called) return
                    called = true
                    resolvePromise(promise2, y, resolve, reject)
                }, (r) => {
                    if (called) return
                    called = true
                    reject(r)
                })
            }else {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}
  1. 首先,先判断新返回的一个promisepromise2是不是等于x,抛出错误UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>,这一步是防止内部的循环引用。
  2. 声明一个变量called,相当于加了一把锁,让promise只能调用一次成功或者失败回调,防止死循环。
  3. 解析x,如果它的类型是object并且不为null,或者它是一个函数,并且它有then方法,我们认为这是一个promise
  4. 递归解析,then里面再次调用resolvePromise

3.4 版本四:模拟异步微任务

因为promiseEventLoop里面是个微任务,不过我们可以简单通过setTimeout模拟。

然后我们再加上一些报错的捕获代码以及一些参数的兼容代码,以及实现catch方法。

js 复制代码
class Promise {
    constructor(executor) {
        // ...
        // 这里增加try catch
        try {
            executor(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }
    then(onFulfilled, onRejected) {
        // 这里兼容下 onFulfilled 和 onRejected 的传参
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
        onRejected = typeof onRejected === 'function' ? onRejected : err => {
            throw err
        }
        const promise2 = new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                // 用 setTimeout 模拟异步
                setTimeout(() => {
                    try {
                        const x = onFulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)


            }
            if (this.status === REJECTED) {
                // 用 setTimeout 模拟异步
                setTimeout(() => {
                    try {
                        const x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)
            }
            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() => {
                    // 用 setTimeout 模拟异步
                    setTimeout(() => {
                        try {
                            const x = onFulfilled(this.value)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)
                })
                this.onRejectedCallbacks.push(() => {
                    // 用 setTimeout 模拟异步
                    setTimeout(() => {
                        try {
                            const x = onRejected(this.reason)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)


                })
            }
        })

        return promise2
    }
    // catch函数实际上里面就是调用了then方法
    catch (errCallback) {
        return this.then(null, errCallback)
    }
}
  1. executor执行时增加try catch,防止执行用户传入的函数直接就报错了,这时我们应该直接reject promise。
  2. 调用onFulfilledonRejected时,需要包裹setTimeout
  3. catch函数实际上里面就是调用了then方法,然后第一个参数传null

ok,这样就写的差不多了。最后我们来测试下我们写的promise是否符合规范。

4、测试 promise,使其符合 Promises/A+ 规范

promise是有规范的,即Promises/A+,我们可以跑一段脚本测试写的promise是否符合规范。

首先,需要在我们的promise增加如下代码:

js 复制代码
// 测试脚本
Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

然后安装promises-aplus-tests包,比如用npm可以使用命令npm install -g promises-aplus-tests安装到全局,然后使用命令promises-aplus-tests 文件名即可进行测试,里面有872测试用例,全部通过即可以认为这是一个标准的promise

测试全部通过,大功告成了!

相关推荐
粉末的沉淀1 小时前
tauri:tauri2.0+vue3+vite打包案例
前端
丫丫7237341 小时前
Raycaster(鼠标点击选中模型)
javascript·webgl
北慕阳2 小时前
选择采购单按钮
前端·javascript·数据库
华仔啊2 小时前
Vite 到底能干嘛?为什么 Vue3 官方推荐它做工程化工具?
前端·javascript·vue.js
悟能不能悟2 小时前
目前流行的前端框架
开发语言·javascript·ecmascript
小明的小名叫小明2 小时前
区块链核心知识点梳理(面试高频考点4)-以太坊交易全流程
面试·区块链
ZXH01222 小时前
性能提升60%:性能优化指南
前端·性能优化
棋啊_Rachel2 小时前
面试高频详解:Redis 缓存击穿、雪崩、穿透
redis·缓存·面试
赵庆明老师2 小时前
NET 中深度拷贝一个对象
前端·javascript·ui