前端高频面试题之手写Promise

1、什么是 promise?

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

2、Promise 的特点

特点一:Promise 有三种状态。

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

特点二:Promise 状态不可逆。

  • promise的状态一旦发生变更,便无法再更改。比如调用resolve从pending变为fulfilled,它的状态就永远是fulfilled了,再调用reject也无法从fulfilled变成rejected。
  • 并且状态只能从pending 变为fulfilled或rejected,不能从fulfilled或rejected返回到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方法,并且会传入resolve和reject两个函数作为调用参数,另外在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 ,那么上面 onFulfilled 和 onRejected 将无法执行了。

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)
        })
       }
    }
}

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

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 版本四:模拟异步微任务

因为promise在EventLoop里面是个微任务,不过我们可以简单通过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. 调用onFulfilled和onRejected时,需要包裹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。

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

相关推荐
damoluomu4 分钟前
静态站点生成器怎么选:Node.js 生态里常用的五个(番外一)
javascript·css·typescript·node.js·html
小马过河R17 分钟前
EdgeOne深度解析:从原理、适用场景到COS接入的技术逻辑
java·开发语言·前端·spring·cos
ZoeLandia35 分钟前
TypeScript 04:类型断言与类型守卫
开发语言·前端·typescript
guzicheng199038 分钟前
webpack 安装笔记:全局 vs 本地,以及一个 PATH 环境变量的坑
前端·webpack
西索斯coding44 分钟前
Claude Code 工具调用返回空怎么办?claude-opus-5.5 的 input_schema 校验踩坑记录与修复方案
服务器·前端·javascript·ai
疯狂敲代码的老刘1 小时前
MarvellousSuspender浏览器扩展:1.4K星开源标签页冻结神器,Chrome吃内存终结者的干净复活版
前端·chrome
码上上班1 小时前
shell 脚本知识点总结
前端·chrome
程序员Sunday1 小时前
Flex 和 Grid 有什么区别?一维布局、二维布局怎么选?
前端·css·面试
Jlzn88881 小时前
华南PACK线自动化率怎么统计才靠谱
前端·javascript·自动化
拖孩1 小时前
AI 时代,个人开发者到底能不能靠一个小程序赚到钱
前端·后端·微信小程序