You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript自定义Promise类实现异常:无setTimeout包裹resolve时then方法重复打印日志

问题根源分析

你的问题出在同步 resolve 时,then 方法会重复执行所有已注册的回调,加上你的 then 实现没有遵循 Promise 规范返回新实例,导致回调被多次调用。我们一步步拆解流程:

同步 resolve 的执行流程

当你直接调用 resolve(1) 而不用 setTimeout 时,执行顺序是这样的:

  1. 创建 CustomPromise 实例,立即执行构造函数里的回调 cb(this.#resolve),也就是直接调用 resolve(1)。
  2. #resolve 方法更新状态为 RESOLVED,值设为 1,然后尝试执行 #thenCallbacks.reduce —— 但这时候 #thenCallbacks 是空的(第一个 then 还没执行),所以这次遍历没有效果。
  3. 构造函数执行完毕,开始调用第一个 .then():
    • 把第一个回调(打印 first value)push 到 #thenCallbacks。
    • 此时状态已经是 RESOLVED,所以执行 #thenCallbacks.reduce,遍历数组里的1个回调,执行它,输出 first value 1,返回 2。
  4. 接着调用第二个 .then():
    • 把第二个回调(打印 second value)push 到 #thenCallbacks。
    • 状态依然是 RESOLVED,所以再次执行 #thenCallbacks.reduce,这次数组里有2个回调:先执行第一个(再次输出 first value 1),再执行第二个(输出 second value 2)。

这就是为什么你会看到第一个日志输出两次的原因!

异步 resolve 为什么正常?

当用 setTimeout 包裹 resolve 时,resolve 会被放到宏任务队列,等待同步代码执行完才会执行:

  1. 构造函数执行时,只是把 resolve 加入宏任务队列,状态保持 PENDING。
  2. 两个 .then() 依次执行,把两个回调都 push 到 #thenCallbacks,但因为状态是 PENDING,不会立即执行回调。
  3. 同步代码执行完毕,宏任务队列开始执行 resolve(1):
    • 更新状态为 RESOLVED,然后执行 #thenCallbacks.reduce,依次执行两个回调,每个只执行一次,完全符合预期。

修复方案

要解决这个问题,我们需要做两个关键修改:

  1. then 方法应该返回一个新的 CustomPromise 实例,遵循 Promise 的链式调用规范(这也是标准 Promise 不会出现重复回调的核心原因之一)。
  2. 当状态已经是 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 15:12:46