技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

Promise 实现原理(附源码)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/54-Promise实现原理(附源码)
本文目录10 个章节
  1. 一、Promise 的基本结构
  2. 二、Promise 的状态和值
  3. 三、then 方法的规则
  4. 四、Promise 的解析过程
  5. 五、先实现状态、值和回调队列
  6. 六、一个可运行的 Promise/A+ 风格实现
  7. 七、原文实现中需要特别修正的地方
  8. 八、原生 Promise 与手写实现的边界
  9. 总结
  10. 参考资料

Promise 实现原理(附源码)

Category(分类): JavaScript Status: 已整理(2026)

  • 原文发表于 2019 年,2022 年 6 月 24 日更新。
  • 原文作者已经说明:早期源码并不完全符合 Promise/A+,并推荐继续阅读完全符合 Promise A+ 规范的 Promise 源码实现
  • 本文尽量保留原文“从状态、值、队列到 then,再到 catch、静态方法和完整源码”的学习路径,同时修复抓取造成的代码拼接、拼写错误、同步回调、instanceof 判断 thenable、空数组和状态竞争等问题。

原文:Promise 实现原理(附源码)

本文的手写实现用于理解 Promise 的核心机制,不建议替换生产环境的原生 Promise。原生实现还涉及规范中的任务调度、异步 thenable 调用、迭代器关闭、子类化、Symbol.species 等更多细节。

一、Promise 的基本结构

构造 Promise 时,需要传入一个 executor 函数。executor 会立即同步执行,并接收 resolvereject 两个函数;真正的异步性通常来自定时器、网络请求、文件读写等操作:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('FULFILLED')
  }, 1000)
})

promise.then(value => {
  console.log(value) // 约 1 秒后:FULFILLED
})

构造器的返回值是 Promise 对象,executor 的返回值会被忽略。如果 executor 抛出异常,且此前还没有调用 resolving function,Promise 会变为 rejected:

const failed = new Promise(() => {
  throw new Error('executor failed')
})

failed.catch(error => console.log(error.message)) // executor failed

原文把 executor 称为 handle;两者指的是同一个概念。构造器只能接收函数:

try {
  new Promise('not a function')
} catch (error) {
  console.log(error.name) // TypeError
}

二、Promise 的状态和值

Promise 常用的三种状态是:

  • pending:进行中;
  • fulfilled:已成功;
  • rejected:已失败。

状态只能从 pending 变化为 fulfilledrejected,并且一旦确定就不能反向变化。resolvereject 传入的值分别是 fulfillment value 和 rejection reason:

const success = new Promise(resolve => {
  resolve('success value')
})

const failure = new Promise((resolve, reject) => {
  reject(new Error('failure reason'))
})

success.then(value => console.log(value))
failure.catch(error => console.log(error.message))

2.1 “已解决”和“已敲定”不是完全相同的概念

入门文章经常把 resolve 直接翻译成“成功”,但规范语义更精确:调用 resolve(anotherPromise) 会让外层 Promise 锁定为跟随内层 Promise,此时外层可能仍然是 pending;只有内层最终 fulfilled 或 rejected,外层才会敲定:

const inner = new Promise(resolve => {
  setTimeout(() => resolve('inner value'), 100)
})

const outer = new Promise(resolve => {
  resolve(inner)
})

console.log(outer) // 仍可能显示 pending
outer.then(value => console.log(value)) // inner value

因此,以下描述需要区分:

  • resolved(已解决):resolve function 已经被调用,结果可能是普通值,也可能是正在跟随的 thenable;
  • settled(已敲定):最终已经 fulfilled 或 rejected;
  • 平时说“Promise 状态”时,通常讨论的是 pending、fulfilled、rejected 三种可观察状态。

2.2 resolving function 只接受第一次调用

第一次调用 resolvereject 会锁定结果。即使第一次 resolve 正在等待一个 pending thenable,后面的 reject 也不会抢先生效:

let resolveOuter
let rejectOuter

const outer = new Promise((resolve, reject) => {
  resolveOuter = resolve
  rejectOuter = reject
})

resolveOuter(new Promise(resolve => {
  setTimeout(() => resolve('first result'), 10)
}))
rejectOuter(new Error('ignored'))

outer.then(value => console.log(value)) // first result

三、then 方法的规则

then 接收两个可选参数:

promise.then(onFulfilled, onRejected)

3.1 参数不是函数时的默认行为

如果 onFulfilled 不是函数,成功值应原样穿透;如果 onRejected 不是函数,错误应继续抛向后续链:

Promise.resolve('value')
  .then('not a function')
  .then(value => console.log(value)) // value

Promise.reject(new Error('failed'))
  .then(undefined, 'not a function')
  .catch(error => console.log(error.message)) // failed

3.2 每个 then 返回新的 Promise

then 不会修改并返回原 Promise,而是返回一个新的 Promise。这样每条链可以独立发展:

const promise1 = Promise.resolve(1)
const promise2 = promise1.then(value => value + 1)

console.log(promise1 === promise2) // false

promise2.then(value => console.log(value)) // 2

同一个 Promise 也可以注册多个监听者,监听者之间互不覆盖:

const source = new Promise(resolve => {
  setTimeout(() => resolve(1), 10)
})

source.then(value => console.log('first:', value))
source.then(value => console.log('second:', value))

3.3 处理函数的返回值

如果处理函数返回普通值,新 Promise 会 fulfilled,并把这个值交给下一个处理函数:

const promise1 = new Promise(resolve => {
  setTimeout(() => resolve(), 1000)
})

const promise2 = promise1.then(() => {
  return '这里返回一个普通值'
})

promise2.then(value => {
  console.log(value) // 约 1 秒后
})

如果处理函数返回 Promise 或其他 thenable,新 Promise 会等待它完成:

const promise1 = new Promise(resolve => {
  setTimeout(() => resolve(), 1000)
})

const promise2 = promise1.then(() => {
  return new Promise(resolve => {
    setTimeout(() => resolve('这里返回一个 Promise'), 2000)
  })
})

promise2.then(value => {
  console.log(value) // 约 3 秒后
})

不能只用 x instanceof Promise 判断返回值。原生 Promise、其他实现、跨 Realm 的对象以及普通 thenable 都可能不是当前自定义类的实例。真正需要判断的是:对象或函数是否有一个可调用的 then 属性。

3.4 处理函数抛出的异常

如果 onFulfilledonRejected 抛出异常,then 返回的新 Promise 必须 rejected:

const promise1 = new Promise(resolve => {
  setTimeout(() => resolve('success'), 1000)
})

const promise2 = promise1.then(() => {
  throw new Error('这里抛出一个异常')
})

promise2.then(undefined, error => {
  console.log(error.message) // 这里抛出一个异常
})

推荐在链尾使用 catch 统一处理:

promise1
  .then(value => processValue(value))
  .catch(error => console.error(error))

function processValue(value) {
  return value.toUpperCase()
}

3.5 处理函数必须异步调用

即使源 Promise 已经 fulfilled,then 注册的处理函数也不会在当前调用栈同步执行:

const order = []

Promise.resolve().then(() => order.push('then'))
order.push('sync')

console.log(order) // ['sync']

setTimeout(() => {
  console.log(order) // ['sync', 'then']
}, 0)

原文早期实现使用 setTimeout 模拟异步。在现代实现中,Promise reaction 通常使用微任务(Promise jobs);setTimeout 是任务,两者的调度时机和优先级不同。手写示例使用 queueMicrotask 更接近原生行为,但它要求现代运行环境。

四、Promise 的解析过程

Promise/A+ 用 [[Resolve]](promise2, x) 描述 then 返回值的处理过程。可以概括为:

  1. 如果 x 就是 promise2,拒绝并报告循环引用;
  2. 如果 x 是对象或函数,读取它的 then 属性;
  3. 如果 then 可调用,把 x 当作 thenable,并只接受第一次成功或失败回调;
  4. thenable 成功传入的值还要递归解析;
  5. then 属性读取或调用过程抛错时,拒绝 promise2
  6. 其他值直接使 promise2 fulfilled。

下面是一个独立的解析函数。它解释了原文源码中“返回另一个 Promise 时继续等待”的核心:

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    reject(new TypeError('Promise cannot resolve to itself'))
    return
  }

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    let then
    let called = false

    try {
      then = x.then
    } catch (error) {
      reject(error)
      return
    }

    if (typeof then === 'function') {
      try {
        then.call(
          x,
          value => {
            if (called) return
            called = true
            resolvePromise(promise2, value, resolve, reject)
          },
          reason => {
            if (called) return
            called = true
            reject(reason)
          }
        )
      } catch (error) {
        if (called) return
        called = true
        reject(error)
      }
      return
    }
  }

  resolve(x)
}

called 不能省略。thenable 是普通用户对象,它可能错误地同时调用成功和失败回调,也可能多次调用;Promise 解析过程必须只采用第一次结果。

上面的函数展示的是解析规则本身;原生 ECMAScript 还会把 thenable 的调用安排为 Promise job。手写实现如果希望更接近原生行为,也应避免在 resolve(thenable) 的当前调用栈中直接执行 thenable 的 then

五、先实现状态、值和回调队列

原文先定义状态常量和 isFunction 工具函数,这个拆分思路仍然适合学习:

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

const isFunction = value => typeof value === 'function'

一个只实现状态转换的最小版本如下。它还没有 then 和 thenable 解析,只用于观察状态不能二次改变:

class StatePromise {
  constructor(executor) {
    if (!isFunction(executor)) {
      throw new TypeError('StatePromise executor must be a function')
    }

    this.status = PENDING
    this.value = undefined

    const fulfill = value => {
      if (this.status !== PENDING) return
      this.status = FULFILLED
      this.value = value
    }

    const reject = reason => {
      if (this.status !== PENDING) return
      this.status = REJECTED
      this.value = reason
    }

    try {
      executor(fulfill, reject)
    } catch (error) {
      reject(error)
    }
  }
}

原始文章中类似 _resolve_reject 的方法会直接改变 _status_value。完整实现还必须考虑:

  • resolve 传入 thenable 时不能马上 fulfilled;
  • executor 后续抛错不能覆盖已经调用过的 resolve
  • pending 时要保存多个成功、失败处理器;
  • fulfilled/rejected 后新增的 then 仍要异步执行;
  • 一个处理器抛错或返回 thenable 时,必须决定下一个 Promise 的状态。

六、一个可运行的 Promise/A+ 风格实现

下面的 MyPromise 保留原文的队列、catchallracefinally 结构,并补齐 thenable 采纳、状态锁定、异步回调、普通值、空 iterable 和循环引用处理。它是学习版本,不是完整的原生 Promise polyfill。

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

class MyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('MyPromise executor must be a function')
    }

    this.status = PENDING
    this.value = undefined
    this.reason = undefined
    this.handlers = []

    let alreadyResolved = false

    const fulfill = value => {
      if (this.status !== PENDING) return
      this.status = FULFILLED
      this.value = value
      this.flush()
    }

    const rejectInternal = reason => {
      if (this.status !== PENDING) return
      this.status = REJECTED
      this.reason = reason
      this.flush()
    }

    const resolveValue = value => {
      if (value === this) {
        rejectInternal(new TypeError('Promise cannot resolve to itself'))
        return
      }

      if (value !== null &&
        (typeof value === 'object' || typeof value === 'function')) {
        let then

        try {
          then = value.then
        } catch (error) {
          rejectInternal(error)
          return
        }

        if (typeof then === 'function') {
          let called = false

          queueMicrotask(() => {
            try {
              then.call(
                value,
                nextValue => {
                  if (called) return
                  called = true
                  resolveValue(nextValue)
                },
                reason => {
                  if (called) return
                  called = true
                  rejectInternal(reason)
                }
              )
            } catch (error) {
              if (!called) {
                called = true
                rejectInternal(error)
              }
            }
          })
          return
        }
      }

      fulfill(value)
    }

    const resolve = value => {
      if (alreadyResolved) return
      alreadyResolved = true
      resolveValue(value)
    }

    const reject = reason => {
      if (alreadyResolved) return
      alreadyResolved = true
      rejectInternal(reason)
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  flush() {
    const handlers = this.handlers.splice(0)
    for (const handler of handlers) {
      queueMicrotask(handler)
    }
  }

  then(onFulfilled, onRejected) {
    const fulfilled = typeof onFulfilled === 'function'
      ? onFulfilled
      : value => value
    const rejected = typeof onRejected === 'function'
      ? onRejected
      : reason => { throw reason }

    let resolveNext
    let rejectNext
    const promise2 = new MyPromise((resolve, reject) => {
      resolveNext = resolve
      rejectNext = reject
    })

    const run = () => {
      try {
        const callback = this.status === FULFILLED ? fulfilled : rejected
        const input = this.status === FULFILLED ? this.value : this.reason
        resolveNext(callback(input))
      } catch (error) {
        rejectNext(error)
      }
    }

    if (this.status === PENDING) {
      this.handlers.push(run)
    } else {
      queueMicrotask(run)
    }

    return promise2
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }

  finally(onFinally) {
    if (typeof onFinally !== 'function') return this.then()

    return this.then(
      value => MyPromise.resolve(onFinally()).then(() => value),
      reason => MyPromise.resolve(onFinally()).then(() => {
        throw reason
      })
    )
  }

  static resolve(value) {
    return value instanceof MyPromise
      ? value
      : new MyPromise(resolve => resolve(value))
  }

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

  static all(iterable) {
    const values = Array.from(iterable)

    return new MyPromise((resolve, reject) => {
      if (values.length === 0) {
        resolve([])
        return
      }

      const result = new Array(values.length)
      let remaining = values.length

      values.forEach((value, index) => {
        MyPromise.resolve(value).then(item => {
          result[index] = item
          remaining -= 1
          if (remaining === 0) resolve(result)
        }, reject)
      })
    })
  }

  static race(iterable) {
    return new MyPromise((resolve, reject) => {
      for (const value of iterable) {
        MyPromise.resolve(value).then(resolve, reject)
      }
    })
  }

  static deferred() {
    const deferred = {}
    deferred.promise = new MyPromise((resolve, reject) => {
      deferred.resolve = resolve
      deferred.reject = reject
    })
    return deferred
  }
}

6.1 基本使用

MyPromise.resolve(1)
  .then(value => value + 1)
  .then(value => console.log(value)) // 2

MyPromise.reject(new Error('failed'))
  .catch(error => console.log(error.message)) // failed

MyPromise.all([
  MyPromise.resolve('a'),
  2,
  new MyPromise(resolve => setTimeout(() => resolve('c'), 10))
]).then(values => console.log(values)) // ['a', 2, 'c']

const deferred = MyPromise.deferred()
deferred.promise.then(value => console.log(value))
deferred.resolve('deferred value')

6.2 边界测试

const thenable = {
  then(resolve, reject) {
    resolve('first')
    reject(new Error('ignored'))
  }
}

MyPromise.resolve(thenable).then(value => console.log(value)) // first

let self
self = MyPromise.resolve().then(() => self)
self.catch(error => console.log(error.name)) // TypeError

MyPromise.all([]).then(values => console.log(values)) // []

七、原文实现中需要特别修正的地方

7.1 队列不能只在状态改变时同步执行

原文使用两个数组保存 onFulfilledonRejected,这个思路是正确的;但以下细节必须补齐:

  • 传入的参数不是函数时,不能直接调用它,要使用值穿透或错误穿透;
  • 处理器必须异步执行;
  • 回调队列需要在执行前取出或清空,避免重复调用;
  • 状态已经改变后再调用 then,仍要排入微任务;
  • 处理器返回的对象需要走 thenable 解析,而不是只判断 instanceof MyPromise

7.2 setTimeout 不是 Promise 微任务的等价实现

setTimeout(fn, 0) 可以粗略地模拟“稍后执行”,也能帮助早期浏览器理解异步队列,但它属于任务(task),通常晚于当前批次的 Promise 微任务。现代代码应使用原生 Promise;教学实现可以使用 queueMicrotask,并注明兼容性边界。

7.3 Promise.allPromise.race 的参数不是只能是自定义 Promise

原生方法接受可迭代对象,元素可以是普通值、原生 Promise、其他 Promise 实现或 thenable。结果顺序由输入索引决定,而不是由完成顺序决定:

Promise.all([
  Promise.resolve('first'),
  2,
  Promise.resolve('third')
]).then(values => {
  console.log(values) // ['first', 2, 'third']
})

Promise.all([]) 会 fulfilled 为 [];空的 Promise.race([]) 会一直 pending。本文学习实现中的 MyPromise.all 先执行 Array.from(iterable),所以非 iterable 或迭代器抛错可能在返回自定义 Promise 前同步抛出,这与原生 Promise.all 返回 rejected Promise 的边界不同。race 只决定外层结果,不会自动取消输掉的网络请求;请求取消应结合 AbortController

7.4 finally 不会接收结果值

finally 的回调不接收 fulfillment value 或 rejection reason。清理完成后,原来的值或错误通常会继续传递;但如果清理回调抛错或返回 rejected Promise,新的错误会覆盖原结果:

Promise.resolve('data')
  .finally(() => console.log('cleanup'))
  .then(value => console.log(value)) // data

八、原生 Promise 与手写实现的边界

  • Promise/A+ 主要描述 then 和 Promise Resolution Procedure;
  • ECMAScript 原生 Promise 还包含静态组合方法、子类化、标准作业队列和与语言规范的整合;
  • 浏览器、Node.js 等宿主还会决定任务、渲染、I/O 阶段等更大的调度环境;
  • queueMicrotaskAbortControllerPromise.allSettledPromise.anyPromise.withResolvers 等现代 API 不应被早期手写实现随意模拟;
  • 生产环境应优先使用原生 Promise,手写版本适合阅读、实验和通过 Promise/A+ 测试理解机制。

总结

  1. Promise 有 pending、fulfilled、rejected 三种状态,状态只能确定一次;
  2. executor 在构造时同步执行,异步性来自其中启动的异步任务;
  3. resolve 一个 thenable 会让当前 Promise 跟随它,而不一定立刻 fulfilled;
  4. then 总是返回新的 Promise,处理器返回值决定下一环;
  5. 处理器不是函数时要执行值穿透或错误穿透;
  6. thenable 必须按协议解析,不能只用 instanceof
  7. Promise 处理器是异步执行的,现代实现通常使用微任务;
  8. allraceallSettledany 适用于不同组合需求;
  9. 手写 Promise 的重点是状态锁定、回调队列、异常传播、thenable 和循环引用。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS