Promise 实现原理(附源码)
Category(分类): JavaScript Status: 已整理(2026)
- 原文发表于 2019 年,2022 年 6 月 24 日更新。
- 原文作者已经说明:早期源码并不完全符合 Promise/A+,并推荐继续阅读完全符合 Promise A+ 规范的 Promise 源码实现。
- 本文尽量保留原文“从状态、值、队列到
then,再到catch、静态方法和完整源码”的学习路径,同时修复抓取造成的代码拼接、拼写错误、同步回调、instanceof判断 thenable、空数组和状态竞争等问题。
本文的手写实现用于理解 Promise 的核心机制,不建议替换生产环境的原生
Promise。原生实现还涉及规范中的任务调度、异步 thenable 调用、迭代器关闭、子类化、Symbol.species等更多细节。
一、Promise 的基本结构
构造 Promise 时,需要传入一个 executor 函数。executor 会立即同步执行,并接收 resolve、reject 两个函数;真正的异步性通常来自定时器、网络请求、文件读写等操作:
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 变化为 fulfilled 或 rejected,并且一旦确定就不能反向变化。resolve 或 reject 传入的值分别是 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 只接受第一次调用
第一次调用 resolve 或 reject 会锁定结果。即使第一次 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 处理函数抛出的异常
如果 onFulfilled 或 onRejected 抛出异常,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 返回值的处理过程。可以概括为:
- 如果
x就是promise2,拒绝并报告循环引用; - 如果
x是对象或函数,读取它的then属性; - 如果
then可调用,把x当作 thenable,并只接受第一次成功或失败回调; - thenable 成功传入的值还要递归解析;
then属性读取或调用过程抛错时,拒绝promise2;- 其他值直接使
promise2fulfilled。
下面是一个独立的解析函数。它解释了原文源码中“返回另一个 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 保留原文的队列、catch、all、race 和 finally 结构,并补齐 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 队列不能只在状态改变时同步执行
原文使用两个数组保存 onFulfilled 和 onRejected,这个思路是正确的;但以下细节必须补齐:
- 传入的参数不是函数时,不能直接调用它,要使用值穿透或错误穿透;
- 处理器必须异步执行;
- 回调队列需要在执行前取出或清空,避免重复调用;
- 状态已经改变后再调用
then,仍要排入微任务; - 处理器返回的对象需要走 thenable 解析,而不是只判断
instanceof MyPromise。
7.2 setTimeout 不是 Promise 微任务的等价实现
用 setTimeout(fn, 0) 可以粗略地模拟“稍后执行”,也能帮助早期浏览器理解异步队列,但它属于任务(task),通常晚于当前批次的 Promise 微任务。现代代码应使用原生 Promise;教学实现可以使用 queueMicrotask,并注明兼容性边界。
7.3 Promise.all 和 Promise.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 阶段等更大的调度环境;
queueMicrotask、AbortController、Promise.allSettled、Promise.any和Promise.withResolvers等现代 API 不应被早期手写实现随意模拟;- 生产环境应优先使用原生 Promise,手写版本适合阅读、实验和通过 Promise/A+ 测试理解机制。
总结
- Promise 有 pending、fulfilled、rejected 三种状态,状态只能确定一次;
- executor 在构造时同步执行,异步性来自其中启动的异步任务;
resolve一个 thenable 会让当前 Promise 跟随它,而不一定立刻 fulfilled;then总是返回新的 Promise,处理器返回值决定下一环;- 处理器不是函数时要执行值穿透或错误穿透;
- thenable 必须按协议解析,不能只用
instanceof; - Promise 处理器是异步执行的,现代实现通常使用微任务;
all、race、allSettled、any适用于不同组合需求;- 手写 Promise 的重点是状态锁定、回调队列、异常传播、thenable 和循环引用。