手写 Promise,深入理解其核心原理

2026-03-02 11:12:31
偶然间翻了一下这篇文章, 有点杂乱无章了。让 ai 润色了一下..

  • 为什么 executor 会立即执行?
  • 为什么 Promise 的状态只能改变一次?
  • 为什么 throw error 会让 Promise 变成失败状态?
  • 为什么 then 可以拿到成功或失败的结果?
  • 为什么 then 可以链式调用?

通过手写一个 MyPromise,一步一步拆解 Promise 的核心原理。

基础结构

1. 从 Promise 的使用方式开始

先回顾一下 Promise 最常见的创建方式:

new Promise((resolve, reject) => {
  // 执行异步或同步任务
})

从这个使用方式中可以看出几个关键信息:

  • Promise 可以通过 new 调用,所以它可以是一个类
  • 构造函数接收一个函数作为参数,这个函数通常叫做 executor
  • executor 会在创建 Promise 时立即执行
  • executor 接收两个参数:resolvereject
  • resolve 用来把任务标记为成功
  • reject 用来把任务标记为失败

所以我们可以先搭建出 MyPromise 的基本结构。

2. 搭建 MyPromise 的基本结构

这一节先不考虑状态变化,也不实现 then 方法,只模拟最基础的调用形式:

new MyPromise((resolve, reject) => {
  // executor 会立即执行
})

因此,我们需要定义一个 MyPromise 类,并在构造函数中立即调用 executor

class MyPromise {
  constructor(executor) {
    executor(this._resolve, this._reject)
  }

  /**
   * 将任务标记为完成
   * @param data 完成时的数据,例如 resolve([1, 2])
   */
  _resolve(data) {

  }

  /**
   * 将任务标记为失败
   * @param reason 任务失败的原因
   */
  _reject(reason) {

  }
}

现在 executor 已经可以立即执行了,但是还没有记录任务状态,也没有保存成功或失败的结果。

3. 增加状态和值

Promise 内部有三种状态:

  • pending:初始状态
  • fulfilled:成功状态
  • rejected:失败状态

状态的流转规则也很简单:

  • 可以从 pending 变成 fulfilled
  • 可以从 pending 变成 rejected
  • 一旦变成 fulfilledrejected,状态就不能再次改变

除了状态之外,Promise 还需要保存成功结果或失败原因。

例如:

resolve(1)
reject(new Error('fail'))

这些值都需要保存下来,后续传给 then 的回调。

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  /**
   * 任务执行器
   * @param executor
   */
  constructor(executor) {
    this._state = PENDING
    this._value = undefined

    executor(this._resolve.bind(this), this._reject.bind(this))
  }

  /**
   * 更新当前状态
   * @param newState 新状态
   * @param value 成功结果或失败原因
   */
  _changeState(newState, value) {
    if (this._state !== PENDING)
      return

    this._state = newState
    this._value = value
  }

  /**
   * 将任务标记为完成
   * @param data 完成时的数据,例如 resolve([1, 2])
   */
  _resolve(data) {
    this._changeState(FULFILLED, data)
  }

  /**
   * 将任务标记为失败
   * @param reason 任务失败的原因
   */
  _reject(reason) {
    this._changeState(REJECTED, reason)
  }
}

这里的 _changeState 是状态切换的统一入口。

它有两个作用:

  1. 修改当前 Promise 的状态
  2. 保存成功结果或失败原因

其中最重要的是这段判断:

if (this._state !== PENDING)
    return

它保证了 Promise 的状态只能从 pending 改变一次。

4. 保证状态只能改变一次

Promise 的状态一旦确定,就不能再次改变。

例如:

const p = new MyPromise((resolve, reject) => {
  resolve('success')
  reject('error')
})

console.log(p)

虽然 executor 中同时调用了 resolvereject,但第一次调用 resolve 时,状态已经从 pending 变成了 fulfilled

所以后面的 reject 会被忽略。

也就是说,当前 Promise 最终保存的状态和值应该是:

{
    _state: 'fulfilled',
    _value: 'success'
}

这也是 _changeState 中判断当前状态是否为 pending 的原因。

5. 捕获 executor 中的异常

原生 Promise 中,如果 executor 执行过程中抛出异常,Promise 会自动变成 rejected 状态。

例如:

const p = new Promise(() => {
  throw new Error('error')
})

所以我们需要在执行 executor 时使用 try...catch 包裹。

class MyPromise {
  constructor(executor) {
    this._state = PENDING
    this._value = undefined

    try {
      executor(this._resolve.bind(this), this._reject.bind(this))
    }
    catch (error) {
      this._reject(error)
    }
  }
}

这样,当 executor 中出现异常时,就会自动调用 _reject,把当前 Promise 标记为失败。

then 方法

then 是 Promise 中最核心的方法,它的作用是注册成功和失败后的回调。

它接收两个参数:

  • onFulfilled:Promise 变成 fulfilled 后执行
  • onRejected:Promise 变成 rejected 后执行

我们使用是这样的:

const p = new Promise((resolve, reject) => {
  resolve(123)
})

p.then(
  (data) => {
    console.log('fulfilled:', data)
  },
  (reason) => {
    console.log('rejected:', reason)
  }
)

1. then 会返回一个新的 Promise

原生 Promise 中,then 方法一定会返回一个新的 Promise

这也是 Promise 可以链式调用的原因:

promise
  .then((value) => {
    return value + 1
  })
  .then((value) => {
    console.log(value)
  })

所以,MyPromisethen 方法也需要返回一个新的 MyPromise

class MyPromise {
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {

    })
  }
}

2. pending 状态下需要保存回调

如果调用 then 时,Promise 已经是 fulfilledrejected 状态,那么可以直接执行对应的回调。

但更多时候,调用 then 时 Promise 还处于 pending 状态。

比如:

const p = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve(123)
  }, 2000)
})

p.then((data) => {
  console.log(data)
})

调用 then 的时候,setTimeout 还没有执行,Promise 仍然是 pending

所以此时不能马上执行 onFulfilled,而是要先把它保存起来。

等后续调用 resolve 改变状态后,再统一执行这些回调。

因此,我们需要在 MyPromise 中增加一个队列,用来保存通过 then 注册的任务。

class MyPromise {
  constructor(executor) {
    this._state = PENDING
    this._value = undefined
    this._handlers = []

    try {
      executor(this._resolve.bind(this), this._reject.bind(this))
    }
    catch (error) {
      this._reject(error)
    }
  }
}

3. 保存 then 注册的回调任务

每次调用 then,其实都是注册两个可能执行的任务:

  • Promise 成功时执行 onFulfilled
  • Promise 失败时执行 onRejected

同时,因为 then 返回了一个新的 Promise,所以我们还需要保存这个新 Promise 的 resolvereject

这样后面执行回调时,才能决定让 then 返回的新 Promise 成功还是失败。

class MyPromise {
  constructor(executor) {
    this._state = PENDING
    this._value = undefined
    this._handlers = []

    try {
      executor(this._resolve.bind(this), this._reject.bind(this))
    }
    catch (error) {
      this._reject(error)
    }
  }

  /**
   * 注册成功和失败回调
   * @param onFulfilled 成功回调
   * @param onRejected 失败回调
   */
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this._pushHandler(onFulfilled, FULFILLED, resolve, reject)
      this._pushHandler(onRejected, REJECTED, resolve, reject)
    })
  }

  /**
   * 保存 then 注册的回调任务
   * @param executor 当前需要执行的回调
   * @param state 这个回调在哪种状态下执行
   * @param resolve then 返回的新 Promise 的 resolve
   * @param reject then 返回的新 Promise 的 reject
   */
  _pushHandler(executor, state, resolve, reject) {
    this._handlers.push({ executor, state, resolve, reject })
  }
}

到这里,我们只是把 then 注册的回调保存起来了,还没有真正执行它们。

下一步需要在 Promise 状态改变后,从 _handlers 中找到对应状态的任务并执行。

4. 确定回调的执行时机

前面我们已经把 then 注册的回调保存到了 _handlers 队列中。

接下来要解决的问题是:什么时候执行这些回调?

有两个时机需要触发队列执行。

第一个时机是:Promise 状态发生变化时。

也就是从 pending 变成 fulfilledrejected 时,需要尝试执行队列中的任务。

例如:

const p = new MyPromise((resolve) => {
  setTimeout(() => {
    resolve(123)
  }, 2000)
})

p.then((data) => {
  console.log(data)
})

调用 then 时,Promise 还是 pending,所以回调会先进入 _handlers 队列。

等到 resolve(123) 执行时,Promise 状态变成 fulfilled,这时就需要执行队列中的回调。

因此,我们需要在 _changeState 中调用 _runHandlers

class MyPromise {
  _changeState(newState, value) {
    if (this._state !== PENDING)
      return

    this._state = newState
    this._value = value

    this._runHandlers()
  }

  _runHandlers() {
    if (this._state === PENDING)
      return
  }
}

_runHandlers 中先判断当前状态是否仍然是 pending

如果还是 pending,说明结果还没有产生,回调不能执行,直接返回。

第二个时机是:Promise 状态已经确定之后,再调用 then

例如:

const p = new MyPromise((resolve) => {
  resolve(1)
})

p.then((value) => {
  console.log(value)
})

这个例子中,resolve(1) 是同步执行的。

也就是说,当后面调用 then 时,Promise 已经是 fulfilled 状态了。

如果只在 _changeState 中调用 _runHandlers,这个 then 注册的回调就不会被执行。

所以,每次调用 then 保存完回调后,也需要再调用一次 _runHandlers

class MyPromise {
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this._pushHandler(onFulfilled, FULFILLED, resolve, reject)
      this._pushHandler(onRejected, REJECTED, resolve, reject)
      this._runHandlers()
    })
  }
}

5. 将 then 回调放入微任务队列

原生 Promise 中,then 的回调不会立即执行。

例如:

Promise.resolve().then(() => {
  console.log('then')
})

console.log('end')

输出结果是:

end
then

这说明 then 注册的回调会等当前同步代码执行完后,再进入微任务队列执行。

为了模拟这个行为,我们可以封装一个 runMicroTask 方法,用来把回调放入微任务队列。

function runMicroTask(cb) {
  if (typeof process !== 'undefined' && typeof process.nextTick === 'function') {
    process.nextTick(cb)
  }
  else if (typeof MutationObserver !== 'undefined') {
    const p = document.createElement('p')
    const observer = new MutationObserver(cb)

    observer.observe(p, { childList: true })
    p.innerHTML = '1'
  }
  else {
    setTimeout(cb, 0)
  }
}

这里的处理顺序是:

  1. 如果是 Node.js 环境,优先使用 process.nextTick
  2. 如果是浏览器环境,优先使用 MutationObserver
  3. 如果以上方式都不可用,则降级为 setTimeout

6. 执行队列中的任务

确定执行时机之后,就可以开始处理 _handlers 队列了。

队列中保存的每一项大概长这样:

{
    executor: onFulfilled,
    state: 'fulfilled',
    resolve,
    reject
}

其中:

  • executor 是当前需要执行的回调
  • state 表示这个回调在哪种状态下执行
  • resolve 用来让 then 返回的新 Promise 成功
  • reject 用来让 then 返回的新 Promise 失败

所以执行队列时,需要做几件事:

  1. 如果当前 Promise 还是 pending,不执行
  2. _handlers 中取出任务
  3. 判断任务的 state 是否和当前 Promise 状态一致
  4. 如果状态一致,就把任务放入微任务队列执行
  5. 执行完成后,从队列中删除这个任务
class MyPromise {
  _runHandlers() {
    if (this._state === PENDING)
      return

    while (this._handlers[0]) {
      this._runHandlerItem(this._handlers[0])
      this._handlers.shift()
    }
  }

  _runHandlerItem(handler) {
    runMicroTask(() => {
      const { executor, state, resolve, reject } = handler
      if (this._state !== state)
        return
      // 后面继续处理 executor 的执行逻辑
    })
  }
}

这里先把队列执行流程搭出来。

真正执行单个任务时,还需要继续处理几个问题:

  • then 参数不是函数怎么办?
  • 回调正常返回普通值怎么办?
  • 回调返回 Promise 怎么办?
  • 回调执行过程中抛出异常怎么办?

7. 处理 then 参数不是函数的情况

then 的两个参数都是可选的。

例如:

promise
  .then()
  .then((value) => {
    console.log(value)
  })

第一个 then 没有传入任何回调,但成功的值仍然需要继续向后传递。

这叫做值穿透。

失败状态也是一样的:

promise
  .then(null)
  .then(null, (reason) => {
    console.log(reason)
  })

第一个 then 没有处理失败原因,所以失败原因也需要继续向后传递。

这叫做异常穿透。

所以如果 executor 不是函数,就不能直接调用,而是根据当前 Promise 的状态继续传递结果。

if (typeof executor !== 'function') {
    this._state === FULFILLED
        ? resolve(this._value)
        : reject(this._value)

    return
}

8. 处理回调返回 Promise 的情况

如果 executor 是函数,就可以正常执行它。

最简单的情况是回调返回一个普通值:

promise
  .then((value) => {
    return value + 1
  })
  .then((value) => {
    console.log(value)
  })

这时只需要把回调的返回值作为 then 返回的新 Promise 的成功结果。

const result = executor(this._value)
resolve(result)

但是,then 回调也可能返回一个 Promise。

例如:

const p = new MyPromise((resolve) => {
  resolve(1)
})

p.then((value) => {
  return new MyPromise((resolve) => {
    resolve(value + 1)
  })
}).then((value) => {
  console.log(value)
})

第一个 then 的回调返回了一个新的 Promise。

这时不能直接把这个 Promise 对象作为结果传给下一个 then,而是要等待它的状态确定:

  • 如果它成功,就让当前 then 返回的新 Promise 成功
  • 如果它失败,就让当前 then 返回的新 Promise 失败

所以需要这样处理:

const result = executor(this._value)

if (isPromise(result)) {
  result.then(resolve, reject)
}
else {
  resolve(result)
}

这里的 isPromise 用来判断一个值是否可以当成 Promise 处理。

为了方便,我们先用是否存在 then 方法来判断:

function isPromise(obj) {
  return !!(obj && typeof obj.then === 'function')
}

最后,把这些逻辑合在一起,_runHandlerItem 可以写成这样:

class MyPromise {
  _runHandlerItem(handler) {
    runMicroTask(() => {
      const { executor, state, resolve, reject } = handler

      if (this._state !== state)
        return

      if (typeof executor !== 'function') {
        this._state === FULFILLED
          ? resolve(this._value)
          : reject(this._value)

        return
      }

      try {
        const result = executor(this._value)

        if (isPromise(result)) {
          result.then(resolve, reject)
        }
        else {
          resolve(result)
        }
      }
      catch (error) {
        reject(error)
      }
    })
  }
}

这样就可以同时处理:

  • 回调返回普通值
  • 回调返回 Promise
  • 回调执行过程中抛出异常

9. 当前 then 实现小结

截止这里,then 已经具备了几个核心能力:

  • 可以注册成功和失败回调
  • 可以在 pending 状态下先保存回调
  • 可以在状态改变后执行对应回调
  • 可以保证回调在微任务中执行
  • 可以处理 then 参数不是函数时的值穿透和异常穿透
  • 可以处理回调返回普通值
  • 可以处理回调返回 Promise
  • 可以捕获回调执行过程中的异常

实例方法

1. catch 方法

catch 用来注册失败回调。

它本质上就是只传入失败回调的 then

promise.then(null, onRejected)

所以实现非常简单:

class MyPromise {
  catch(onRejected) {
    return this.then(null, onRejected)
  }
}

2. finally 方法

finally 的特点是:无论 Promise 最终成功还是失败,都会执行传入的回调。

并且基础情况下,finally 不会改变原来的结果。

例如:

MyPromise.resolve(1)
  .finally(() => {
    console.log('finally')
  })
  .then((value) => {
    console.log(value)
  })

最终 then 里拿到的还是 1

基础实现如下:

class MyPromise {
  finally(onSettled) {
    return this.then(
      (data) => {
        onSettled()
        return data
      },
      (reason) => {
        onSettled()
        throw reason
      }
    )
  }
}

静态方法

1. MyPromise.resolve

MyPromise.resolve 用来返回一个成功状态的 Promise。

最简单的情况是传入普通值:

MyPromise.resolve(1)

等价于:

new MyPromise((resolve) => {
  resolve(1)
})

不过它还有两个特殊情况:

  1. 如果传入的本身就是 MyPromise,直接返回它
  2. 如果传入的是 Promise-like 对象,需要等待它的状态
class MyPromise {
  static resolve(data) {
    if (data instanceof MyPromise) {
      return data
    }

    return new MyPromise((resolve, reject) => {
      if (isPromise(data)) {
        data.then(resolve, reject)
      }
      else {
        resolve(data)
      }
    })
  }
}

2. MyPromise.reject

MyPromise.reject 用来返回一个失败状态的 Promise。

class MyPromise {
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }
}

3. MyPromise.all

MyPromise.all 接收一个可迭代对象,返回一个新的 Promise。

它的规则是:

  • 所有任务都成功,最终才成功
  • 最终成功的结果是一个数组
  • 结果数组的顺序和传入顺序一致
  • 只要有一个任务失败,最终就失败

例如:

MyPromise.all([
  MyPromise.resolve(1),
  MyPromise.resolve(2),
  3
]).then((result) => {
  console.log(result)
})

输出:

[1, 2, 3]

实现时需要注意两点:

  1. 每一项都要用 MyPromise.resolve 包一下,因为传入的可能是普通值
  2. 结果要按照原始顺序保存,而不是按照完成顺序保存
class MyPromise {
  static all(proms) {
    return new MyPromise((resolve, reject) => {
      try {
        const results = []
        let count = 0
        let fulfilledCount = 0

        for (const p of proms) {
          const i = count
          count++

          MyPromise.resolve(p).then((data) => {
            fulfilledCount++
            results[i] = data

            if (fulfilledCount === count) {
              resolve(results)
            }
          }, reject)
        }

        if (count === 0) {
          resolve(results)
        }
      }
      catch (error) {
        reject(error)
      }
    })
  }
}

这里的 i 用来记录当前 Promise 在原始数组中的位置。

即使后面的 Promise 先完成,结果也会放回原来的位置。