一起学习造轮子(一):从零开始写一个符合 Promises/A+ 规范的 Promise
Category(分类): JavaScript Status: 已整理
本文保留原文“从基础版本逐步实现 Promise”的学习路线、文件读取案例和 Promises/A+ 条款分析。原文代码中的复制按钮/解读提示等抓取残留、状态竞态、thenable 处理和静态方法边界已修正。
重要范围说明:Promises/A+ 主要规范
then方法和 Promise Resolution Procedure,并不规范构造器、静态all/race方法,也不要求实现必须使用原生 Promise 的 microtask。本文的最终实现是教学用MyPromise,不是原生 Promise 的完整 polyfill。
原文属于“造轮子”系列,配套源码曾托管在 MyWheel。原文的 4 张配图已下载到同目录 images 文件夹;截图是历史材料,不能代替当前规范文字。
前言:为什么要自己实现 Promise
Promise 是处理异步流程的一种抽象。它把“现在还没有结果、以后会成功或失败”的状态封装起来,并允许通过 then 组织后续操作。ES2015 将 Promise 纳入 ECMAScript 标准后,浏览器和 Node.js 都提供了原生实现。
自己实现一个简化版本仍然有学习价值,因为它可以帮助理解:
- pending、fulfilled、rejected 三种状态以及状态不可逆。
then为什么要返回一个新的 Promise,才能形成真正的链。- handler 返回普通值、Promise 或 thenable 时,后继 Promise 如何解析。
- 错误如何穿过链条,以及为什么
catch是then(null, onRejected)的语法糖。 - A+ Resolution Procedure 与 ECMAScript 原生构造器/静态方法的边界。
生产代码应直接使用原生 Promise。下面的阶段代码故意展示演进过程,只有最后的完整实现适合拿来做教学实验,而且仍不应替代标准实现。

一、Promises/A+ 与 ECMAScript Promise 的范围
| 主题 | Promises/A+ | 当前 ECMAScript Promise |
|---|---|---|
then 返回新 Promise | 规范核心 | 支持 |
| handler 异步执行 | 要求调用栈只剩平台代码 | 使用 Promise Jobs/microtasks |
| 2.3 Resolution Procedure | 规范核心 | 以规范内部操作实现 |
构造器 new Promise(executor) | 不负责规定 | 有 alreadyResolved 和 thenable 吸收 |
all、race、resolve、reject | 不属于 A+ 核心 | 静态方法接收 iterable 并处理普通值 |
catch、finally、allSettled、any | 不属于 A+ 核心 | 由 ECMAScript 提供 |
因此,“通过 promises-aplus-tests”只能说明测试覆盖范围内的 then 适配符合 A+,不能证明构造器、thenable、all、race 或 promisify 与原生 Promise 完全相同。
二、基础版本:先保存一个成功/失败回调
目标
- 可以创建 Promise 对象。
- executor 成功时调用成功回调,失败时调用失败回调。
- 先展示最少代码,但明确它还没有状态、链式返回和完整错误处理。
原文的第一版把未注册的回调直接调用,会在同步 resolve 时访问 null。下面保留它的教学意图,但用可选调用让示例不会因为演示而直接崩溃:
function BasicPromise(executor) {
const self = this
self.value = undefined
self.reason = undefined
self.onFulfilled = null
self.onRejected = null
function resolve(value) {
self.value = value
self.onFulfilled?.(value)
}
function reject(reason) {
self.reason = reason
self.onRejected?.(reason)
}
executor(resolve, reject)
}
BasicPromise.prototype.then = function (onFulfilled, onRejected) {
this.onFulfilled = onFulfilled
this.onRejected = onRejected
return this
}
这个版本有意保留了几个缺陷:只能保存一个成功和一个失败回调;同步 resolve 发生在 then 注册之前时,结果不会被补发;返回 this 也不是原生 Promise 的链式语义。它只能作为起点,不能当作 Promise 实现使用。
三、支持同步 executor:回调不能同步执行
原生 Promise 的 executor 会立即执行,但 then 注册的 handler 不会在当前调用栈中同步执行:
const events = []
new Promise((resolve) => {
resolve('done')
}).then(() => events.push('then'))
events.push('after')
queueMicrotask(() => {
console.log(events) // ['after', 'then']
})
A+ 只要求 handler 调用时执行上下文栈已经清空,并没有规定必须使用 microtask。原文使用 setTimeout 达到“晚于当前栈”的效果;更接近现代原生 Promise 的教学调度可以优先使用 queueMicrotask:
const enqueueJob = typeof queueMicrotask === 'function'
? queueMicrotask
: (job) => setTimeout(job, 0)
function schedule(onFulfilled, value) {
enqueueJob(() => onFulfilled?.(value))
}
setTimeout(fn, 0) 是一个后续 task,而 queueMicrotask(fn) 是当前 task 结束后的 microtask。两者都能满足“不是同步调用”的最低要求,但观察到的顺序不相同。
四、支持三种状态
Promise 的状态通常写成:
pending:尚未确定结果。fulfilled:成功完成,并保存 fulfillment value。rejected:失败完成,并保存 rejection reason。
状态只能从 pending 变为 fulfilled 或 rejected,不能从 fulfilled 变回 rejected,也不能从 rejected 变回 fulfilled。
resolved 不一定等于 fulfilled
原生 Promise 还要区分 resolved(已锁定/已解析) 和 settled(已落定):
const inner = new Promise((resolve) => {
setTimeout(() => resolve('inner value'), 100)
})
const outer = new Promise((resolve, reject) => {
resolve(inner) // outer 已经锁定为跟随 inner,但此刻还未 fulfilled
reject(new Error('ignored')) // 第一次 resolve 后被忽略
})
outer.then(console.log) // 约 100ms 后输出 inner value
实现时必须把“第一次 resolve/reject 调用立即锁定”与“最终状态写入”分开。只在 setTimeout 回调里检查 status === pending 是错误的,因为快速连续调用可能同时排队:
// 错误思路:两个调用都在定时器执行前看到 pending
resolve(1)
reject(2)
正确实现需要 alreadyResolved 一次性锁,并在内部 fulfill/rejectInternal 中再次保护最终状态。
五、支持多个观察者和早期“链式”写法
如果一个 Promise 允许多个 then,回调需要保存为数组:
const onFulfilledCallbacks = []
const onRejectedCallbacks = []
onFulfilledCallbacks.push(handler)
for (const callback of onFulfilledCallbacks) {
callback(value)
}
原文第四节在数组基础上返回 this,这样可以写出:
promise.then(f1).then(f2).then(f3)
但这仍然是同一个 Promise 上的回调注册,不是真正的原生链。真正的链要求:
- 每次
then都创建一个新的 Promise。 - 当前 handler 返回的普通值决定下一个 Promise fulfilled 的值。
- handler 返回的 Promise/thenable 被下一个 Promise 吸收。
- handler 抛出的异常让下一个 Promise rejected。
六、then 返回新 Promise:连接异步流程
原文使用 bridgePromise 解释这一点非常有价值。执行:
p.then(f1).then(f2).then(f3)
可以把它理解成:
p.then(f1)创建p2,f1的结果决定p2。p2.then(f2)创建p3,f2的结果决定p3。p3.then(f3)创建p4,f3的结果决定p4。- 如果
f1返回一个 pending Promise,p2会跟随它,后续不会提前执行。
文件读取案例
原文使用 Node.js callback 风格的 fs.readFile 演示串行读取,这是 2018 年常见的写法:
const fs = require('node:fs')
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (error, data) => {
if (error) reject(error)
else resolve(data)
})
})
}
readFile('./file/1.txt')
.then((content) => {
console.log(content)
return readFile('./file/2.txt')
})
.then((content) => {
console.log(content)
return readFile('./file/3.txt')
})
.then(console.log)
.catch(console.error)
当前 Node.js 优先使用 node:fs/promises:
const { readFile } = require('node:fs/promises')
readFile('./file/1.txt', 'utf8')
.then((content) => readFile('./file/2.txt', 'utf8').then((next) => [content, next]))
.then(console.log)
.catch(console.error)
七、Promise Resolution Procedure:解析 handler 返回值
A+ 2.3 的核心是 [[Resolve]](promise, x)。当 handler 返回 x 时,需要按以下顺序处理:
- 如果
x就是待解析的下一个 Promise,拒绝并抛出 TypeError,避免直接循环引用。 - 如果
x是对象或函数,读取一次x.then。 - 读取
then时抛异常,就以该异常拒绝。 - 如果
then不是函数,把x当作普通值 fulfill。 - 如果
then是函数,以x为this调用它。 - thenable 的第一次 resolve/reject 获胜;resolve 得到的
y继续递归解析。 - then 调用已经完成后再次抛出的异常被忽略。
因此,不能只用 x instanceof MyPromise 判断 Promise。跨 iframe 的原生 Promise、其他 Promise 实现和普通 thenable 都可能具有不同构造函数,但只要遵循 then 协议就应该能够互操作。
const thenable = {
then(resolve) {
resolve({
then(resolveAgain) {
resolveAgain(42)
},
})
},
}
Promise.resolve(thenable).then(console.log) // 42
A+ 规范允许实现选择宏任务或微任务来满足异步要求;下面最终实现使用 queueMicrotask 调度 handler,但它仍是教学实现。

八、最终教学实现:状态、锁、thenable 和链式调用
下面的实现集中修复了原文最终版本的关键问题:
resolve/reject第一次调用立即锁定。- 构造器 executor 同步抛错会被捕获;如果此前已经 resolve/reject,则异常被忽略。
- 直接 resolve 普通 thenable 会被递归吸收。
- 自解析会 rejected。
- 每个 handler 只执行一次,settle 后清空队列,减少闭包保留。
- 非函数 handler 使用 identity/thrower 默认行为。
- handler 返回值统一交给 Resolution Procedure。
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const enqueueJob = typeof queueMicrotask === 'function'
? queueMicrotask
: (job) => setTimeout(job, 0)
function isObjectOrFunction(value) {
return (typeof value === 'object' && value !== null) || typeof value === 'function'
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
reject(new TypeError('Promise cannot resolve to itself'))
return
}
if (!isObjectOrFunction(x)) {
resolve(x)
return
}
let then
try {
then = x.then
} catch (error) {
reject(error)
return
}
if (typeof then !== 'function') {
resolve(x)
return
}
let called = false
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)
}
}
class MyPromise {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('MyPromise executor must be a function')
}
this._state = PENDING
this._value = undefined
this._handlers = []
let alreadyResolved = false
const fulfill = (value) => {
if (this._state !== PENDING) return
this._state = FULFILLED
this._value = value
this._flushHandlers()
}
const rejectInternal = (reason) => {
if (this._state !== PENDING) return
this._state = REJECTED
this._value = reason
this._flushHandlers()
}
const resolve = (value) => {
if (alreadyResolved) return
alreadyResolved = true
if (value === this) {
rejectInternal(new TypeError('Promise cannot resolve to itself'))
return
}
resolvePromise(this, value, fulfill, rejectInternal)
}
const reject = (reason) => {
if (alreadyResolved) return
alreadyResolved = true
rejectInternal(reason)
}
try {
executor(resolve, reject)
} catch (error) {
if (!alreadyResolved) {
alreadyResolved = true
rejectInternal(error)
}
}
}
_flushHandlers() {
if (this._state === PENDING) return
const handlers = this._handlers
this._handlers = []
for (const handler of handlers) {
enqueueJob(() => this._runHandler(handler))
}
}
_runHandler(handler) {
const callback = this._state === FULFILLED
? handler.onFulfilled
: handler.onRejected
if (callback === null) {
const passthrough = this._state === FULFILLED
? (value) => handler.resolve(value)
: (reason) => handler.reject(reason)
passthrough(this._value)
return
}
try {
const result = callback(this._value)
resolvePromise(handler.promise, result, handler.resolve, handler.reject)
} catch (error) {
handler.reject(error)
}
}
then(onFulfilled, onRejected) {
const fulfilledHandler = typeof onFulfilled === 'function' ? onFulfilled : null
const rejectedHandler = typeof onRejected === 'function' ? onRejected : null
let nextResolve
let nextReject
const nextPromise = new MyPromise((resolve, reject) => {
nextResolve = resolve
nextReject = reject
})
const handler = {
onFulfilled: fulfilledHandler,
onRejected: rejectedHandler,
promise: nextPromise,
resolve: nextResolve,
reject: nextReject,
}
if (this._state === PENDING) {
this._handlers.push(handler)
} else {
enqueueJob(() => this._runHandler(handler))
}
return nextPromise
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(onFinally) {
const callback = typeof onFinally === 'function' ? onFinally : () => undefined
return this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(reason) => MyPromise.resolve(callback()).then(() => {
throw reason
})
)
}
static resolve(value) {
if (value instanceof this && value.constructor === this) {
return value
}
return new this((resolve) => resolve(value))
}
static reject(reason) {
return new this((resolve, reject) => reject(reason))
}
static all(iterable) {
const Ctor = this
return new Ctor((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
const results = new Array(values.length)
if (values.length === 0) {
resolve(results)
return
}
let remaining = values.length
values.forEach((value, index) => {
Ctor.resolve(value).then(
(result) => {
results[index] = result
remaining -= 1
if (remaining === 0) resolve(results)
},
reject
)
})
})
}
static race(iterable) {
const Ctor = this
return new Ctor((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
for (const value of values) {
Ctor.resolve(value).then(resolve, reject)
}
// 空 iterable 会保持 pending,这是原生 Promise.race 的语义。
})
}
static allSettled(iterable) {
const Ctor = this
return new Ctor((resolve, reject) => {
let values
try {
values = Array.from(iterable)
} catch (error) {
reject(error)
return
}
Ctor.all(values.map((value) => Ctor.resolve(value).then(
(result) => ({ status: 'fulfilled', value: result }),
(reason) => ({ status: 'rejected', reason })
))).then(resolve, reject)
})
}
static promisify(fn, thisArg) {
if (typeof fn !== 'function') {
throw new TypeError('promisify target must be a function')
}
const Ctor = this
return function (...args) {
const receiver = thisArg === undefined ? this : thisArg
return new Ctor((resolve, reject) => {
let callbackCalled = false
const callback = (error, ...values) => {
if (callbackCalled) return
callbackCalled = true
if (error != null) {
reject(error)
} else {
resolve(values.length > 1 ? values : values[0])
}
}
try {
fn.apply(receiver, args.concat(callback))
} catch (error) {
reject(error)
}
})
}
}
static deferred() {
const deferred = {}
deferred.promise = new this((resolve, reject) => {
deferred.resolve = resolve
deferred.reject = reject
})
return deferred
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = MyPromise
}
实现中的几个关键点
1. 为什么要有两套“锁”
alreadyResolved 负责保证 executor 暴露的 resolve/reject 只有第一次调用有效;_state 负责保证内部最终状态只从 pending 转换一次。resolve 一个 pending thenable 时,外层已经 locked-in,但内部仍要等 thenable 的结果才能 fulfill/reject。
2. 为什么 resolvePromise 需要 called
恶意或错误 thenable 可能这样写:
const badThenable = {
then(resolve, reject) {
resolve('first')
reject(new Error('ignored'))
throw new Error('also ignored')
},
}
协议要求第一次 resolve/reject 获胜;called 用来忽略后续调用和已完成后的异常。
3. 为什么必须清空 handler 队列
settle 后先取出队列并清空,再异步执行每个 handler,可以避免 Promise 长期持有已经执行过的闭包。真实引擎还会使用更复杂的内部记录和垃圾回收策略,这里只是教学版的内存保留优化。
4. 为什么 then 不是返回 this
如果返回 this,下面两个 handler 都观察同一个结果,无法表达“第二步等待第一步返回的 Promise”:
const p2 = p.then(() => getUserId())
const p3 = p2.then((id) => getBalance(id))
每一级新 Promise 都是一个桥梁,负责吸收前一级 handler 的返回值。

九、Promises/A+ 2.3 条款逐项对应
最终实现中的 resolvePromise 对应关系如下:
| 条款 | 实现位置 | 含义 |
|---|---|---|
| 2.3.1 | promise2 === x | 禁止链直接解析为自身 |
| 2.3.2 | let then = x.then 的 try/catch | 读取 then 时异常则 reject |
| 2.3.3.1 | typeof then === 'function' | 判断 thenable |
| 2.3.3.2 | 读取异常分支 | then getter 抛错则 reject |
| 2.3.3.3 | then.call(x, ...) | 以 thenable 为 this 调用 |
| 2.3.3.3.1/2 | called | 首次 resolve/reject 获胜 |
| 2.3.3.3.4 | 递归 resolvePromise | resolve 的 y 继续解析 |
| 2.3.3.4 | then 非函数 | 把 x 当普通值 fulfill |
A+ 不要求构造器直接吸收 thenable,但当前实现为了贴近原生 Promise,在构造器 resolve 和静态 resolve 中也使用了同一套算法。
A+ 测试适配器
原文使用:
npx promises-aplus-tests mypromise.js
适配器需要暴露 MyPromise.deferred():
MyPromise.deferred = function () {
const deferred = {}
deferred.promise = new MyPromise((resolve, reject) => {
deferred.resolve = resolve
deferred.reject = reject
})
return deferred
}
promises-aplus-tests 主要测试 then 的异步调用、返回值、异常和 Resolution Procedure。通过该测试不能推出下面这些结论:
- 构造器的
resolve一定正确吸收任意 thenable。 all、race、resolve、reject与原生静态方法完全一致。- 调度顺序与浏览器/Node 原生 Promise microtask 完全一致。
- 子类、跨 realm、内置构造器和
finally等扩展都兼容。
原文截图中的“通过测试”应作为历史记录保留,发布前应使用目标版本的测试套件重新运行,不能把截图当作当前运行证据。

十、all、race、resolve 和 reject
原文第七节把输入当作数组并直接调用 .then,这会漏掉普通值、Set、生成器和空 iterable。当前实现使用 Array.from 和 Ctor.resolve:
MyPromise.all([
MyPromise.resolve(1),
2,
{ then: (resolve) => resolve(3) },
]).then((values) => {
console.log(values) // [1, 2, 3]
})
MyPromise.all([]).then((values) => {
console.log(values) // []
})
MyPromise.race([
new MyPromise((resolve) => setTimeout(() => resolve('slow'), 20)),
'already fulfilled',
]).then(console.log) // already fulfilled
all
- 接收 iterable,而不是只接收带
length的数组。 - 输入顺序决定结果数组顺序,不是完成顺序。
- 普通值会通过
Ctor.resolve变成 fulfilled Promise。 - 空 iterable 立即 fulfilled 为
[]。 - 任意一项 rejected 时整体 rejected;其他已启动的操作不会自动取消。
race
- 第一个 settled 的输入决定结果,可能是 fulfilled 也可能是 rejected。
- 普通值可以立即获胜。
- 空 iterable 永远 pending。
- “最快”指 settle 时间,不是数组中位置,也不意味着底层任务会被取消。
当前原生 API 的扩展
现代 ECMAScript 还提供 finally、allSettled、any 等方法。本文只实现了 allSettled 的教学版本,没有实现完整的 any/AggregateError 语义。需要生产能力时使用原生 Promise,而不是不断扩展这个示例。
十一、promisify:把错误优先回调转换成 Promise
原文把 promisify 的英文名称拼错了,并使用 Bluebird 示例。Node.js 的约定通常是 (error, value):
const fs = require('node:fs')
const { promisify } = require('node:util')
const readFile = promisify(fs.readFile)
readFile('./package.json', 'utf8')
.then((content) => console.log(content.length))
.catch(console.error)
当前 Node.js 对文件系统已经提供了 Promise API,优先写:
const { readFile } = require('node:fs/promises')
async function readPackage() {
const content = await readFile('./package.json', 'utf8')
return content.length
}
教学版 MyPromise.promisify 需要注意:
- 使用
error != null,不能用 truthiness 判断错误。 - 需要保留 receiver 时传入
thisArg,或在方法对象上正确绑定。 - Node 默认通常只返回一个成功值;多成功值 API 应明确返回数组或对象。
- callback 被重复调用时,Promise 的一次性锁应让后续调用失效。
- 已经返回 Promise 的函数不应重复 promisify;非标准 callback 签名需要自定义包装器。
const readFileWithMyPromise = MyPromise.promisify(fs.readFile, fs)
readFileWithMyPromise('./package.json', 'utf8')
.then((content) => console.log(content.length))
.catch(console.error)
十二、建议的回归测试
如果要继续完善这个教学实现,至少应覆盖:
const assert = require('node:assert/strict')
async function test() {
const deferred = MyPromise.deferred()
const events = []
deferred.promise.then(
(value) => events.push(`fulfilled:${value}`),
(reason) => events.push(`rejected:${reason}`)
)
deferred.resolve(1)
deferred.reject(2)
await new Promise((resolve) => setTimeout(resolve, 0))
assert.deepEqual(events, ['fulfilled:1'])
const thenable = { then: (resolve) => resolve(42) }
assert.equal(await MyPromise.resolve(thenable), 42)
assert.deepEqual(await MyPromise.all([1, thenable]), [1, 42])
assert.deepEqual(await MyPromise.all([]), [])
const self = MyPromise.resolve()
const chained = self.then(() => chained)
await assert.rejects(chained, TypeError)
}
test().catch(console.error)
上面只是示意测试,不等于完整 test262 或 A+ 测试套件。尤其是跨 realm、代理、恶意 thenable、子类和特殊内置对象,需要专门的测试矩阵。
总结
从零实现 Promise 最重要的不是把代码写得很短,而是区分几个层次:
- 基础版本用于理解 executor 和回调注册,但不具备 Promise 语义。
- 状态和队列解决多个观察者、状态不可逆和异步 handler。
- 新 Promise 链和
resolvePromise解决返回值传递、thenable 吸收和错误冒泡。 alreadyResolved解决构造器第一次 resolve/reject 的锁定;不能只在异步定时器中检查状态。- A+ 主要覆盖
then与 Resolution Procedure;静态方法、构造器和 Nodepromisify需要另外按 ECMAScript/宿主 API 审计。 - 原生 Promise 已经经过引擎和标准库验证,生产代码不要用本文的
MyPromise替换它。
参考资料: