JavaScript自定义Promise类实现异常:无setTimeout包裹resolve时then方法重复打印日志
问题根源分析
你的问题出在同步 resolve 时,then 方法会重复执行所有已注册的回调,加上你的 then 实现没有遵循 Promise 规范返回新实例,导致回调被多次调用。我们一步步拆解流程:
同步 resolve 的执行流程
当你直接调用 resolve(1) 而不用 setTimeout 时,执行顺序是这样的:
- 创建
CustomPromise实例,立即执行构造函数里的回调cb(this.#resolve),也就是直接调用resolve(1)。 #resolve方法更新状态为RESOLVED,值设为1,然后尝试执行#thenCallbacks.reduce—— 但这时候#thenCallbacks是空的(第一个then还没执行),所以这次遍历没有效果。- 构造函数执行完毕,开始调用第一个
.then():- 把第一个回调(打印
first value)push 到#thenCallbacks。 - 此时状态已经是
RESOLVED,所以执行#thenCallbacks.reduce,遍历数组里的1个回调,执行它,输出first value 1,返回2。
- 把第一个回调(打印
- 接着调用第二个
.then():- 把第二个回调(打印
second value)push 到#thenCallbacks。 - 状态依然是
RESOLVED,所以再次执行#thenCallbacks.reduce,这次数组里有2个回调:先执行第一个(再次输出first value 1),再执行第二个(输出second value 2)。
- 把第二个回调(打印
这就是为什么你会看到第一个日志输出两次的原因!
异步 resolve 为什么正常?
当用 setTimeout 包裹 resolve 时,resolve 会被放到宏任务队列,等待同步代码执行完才会执行:
- 构造函数执行时,只是把
resolve加入宏任务队列,状态保持PENDING。 - 两个
.then()依次执行,把两个回调都 push 到#thenCallbacks,但因为状态是PENDING,不会立即执行回调。 - 同步代码执行完毕,宏任务队列开始执行
resolve(1):- 更新状态为
RESOLVED,然后执行#thenCallbacks.reduce,依次执行两个回调,每个只执行一次,完全符合预期。
- 更新状态为
修复方案
要解决这个问题,我们需要做两个关键修改:
then方法应该返回一个新的CustomPromise实例,遵循 Promise 的链式调用规范(这也是标准 Promise 不会出现重复回调的核心原因之一)。- 当状态已经是
RESOLVED时,只执行当前新增的回调,而不是遍历所有已注册的回调。
下面是修复后的完整代码:
const STATUSES = { PENDING: 'PENDING', RESOLVED: 'RESOLVED', REJECTED: 'REJECTED', } class CustomPromise { #value = null #status = STATUSES.PENDING #thenCallbacks = [] constructor(cb) { // 用setTimeout模拟Promise的异步执行特性,确保同步resolve也会等待回调注册 setTimeout(() => { cb(this.#resolve) }) } #updateState = (status, value) => { // 状态只能从PENDING变更一次,避免重复修改 if (this.#status !== STATUSES.PENDING) return this.#status = status this.#value = value // 状态变更后批量执行所有回调 this.#executeCallbacks() } #resolve = (value) => { this.#updateState(STATUSES.RESOLVED, value) } #executeCallbacks = () => { if (this.#status !== STATUSES.RESOLVED) return // 遍历执行所有回调,更新value为回调的返回值 this.#value = this.#thenCallbacks.reduce((acc, callback) => { return callback(acc) }, this.#value) // 执行完清空回调队列,避免重复执行 this.#thenCallbacks = [] } then = (cb) => { // 返回新的Promise实例,支持链式调用 return new CustomPromise((resolve) => { const wrappedCallback = (value) => { const result = cb(value) // 如果回调返回Promise,等待其resolve后再传递结果 if (result instanceof CustomPromise) { result.then(resolve) } else { resolve(result) } } if (this.#status === STATUSES.PENDING) { this.#thenCallbacks.push(wrappedCallback) } else if (this.#status === STATUSES.RESOLVED) { // 状态已resolved,立即执行当前回调 wrappedCallback(this.#value) } }) } } // 测试代码 new CustomPromise((resolve) => { resolve(1) }) .then((value) => { console.log('first value', value) return 2 }) .then((value) => { console.log('second value', value) return null })
关键修复点说明
- 异步执行构造函数回调:用
setTimeout模拟 Promise 的微任务特性,确保即使同步调用resolve,也会等待所有then回调注册完毕后再执行。 then返回新实例:每个then都返回新的CustomPromise,避免同一个实例的回调队列被重复遍历。- 包裹回调处理返回值:如果回调返回的是 Promise,会等待其 resolve 后再传递结果,符合 Promise 的链式调用逻辑。
- 执行后清空回调队列:避免状态变更后重复执行回调。
这样修改后,不管是同步还是异步调用 resolve,都能保证每个回调只执行一次,完全符合预期。
内容的提问来源于stack exchange,提问作者rio
相关产品推荐
相关产品推荐

