如何手写一个简化版 Promise?
简化版
手写 Promise 的核心是状态机、反应队列和链式决议。状态只有 pending、fulfilled、rejected,且只能从 pending 单向改变;then 必须返回新 Promise,回调要进入微任务;回调返回普通值、新 Promise 或任意 thenable 时,新 Promise 的状态都要按决议过程接管。
详细版
最低限度要实现:执行器同步执行、resolve/reject 只接受第一次调用、pending 时保存回调、settled 后调用 then 仍异步执行、缺省处理器透传值或错误,以及 then 返回新实例。
const p = new MiniPromise(resolve => resolve(1))
const next = p.then(value => value + 1)
next.then(console.log) // 异步输出 2
真正难点不是三个状态常量,而是 Promise Resolution Procedure:禁止新 Promise 用自身完成;读取 then 可能抛错;thenable 可能同时调用成功和失败,必须只认第一次。教学实现也应明确,它没有覆盖原生 Promise 的全部构造器泛化、species、拒绝跟踪和静态组合方法。
完整版教学
一、状态机只是第一层
| 当前状态 | 操作 | 结果 |
|---|---|---|
| pending | fulfill(value) | 变为 fulfilled,结果固定 |
| pending | reject(reason) | 变为 rejected,原因固定 |
| fulfilled/rejected | 再次决议 | 忽略 |
“resolved”不完全等于“fulfilled”。一个 Promise resolve 到另一个 pending Promise 后,已经锁定要跟随它,自己仍可能暂时 pending,但后续 reject 已不能改变结果。
面试实现若在
resolve(thenable)后仍允许同步reject()抢先,就没有做到“第一次决议锁定”。状态何时落定与决议权何时消耗,是两个层次。
二、执行器同步,反应回调异步
console.log('A')
new Promise(resolve => {
console.log('B')
resolve()
}).then(() => console.log('C'))
console.log('D')
// A、B、D、C
构造器中的 executor 立即同步执行;then 回调必须排到当前同步代码之后。浏览器和 Node 中原生 Promise 使用宿主 Promise Job 队列,教学版可用 queueMicrotask 近似调度,不能用同步调用,也不应把 setTimeout 宏任务说成完全等价。
三、一个可运行的教学实现
下面实现覆盖核心链式行为,但有意省略静态组合方法、species 和宿主拒绝跟踪:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MiniPromise {
constructor(executor) {
if (typeof executor !== 'function') throw new TypeError('executor required')
this.state = PENDING
this.value = undefined
this.reactions = []
let alreadyResolved = false
const resolve = value => {
if (alreadyResolved) return
alreadyResolved = true
this.resolveValue(value)
}
const reject = reason => {
if (alreadyResolved) return
alreadyResolved = true
this.rejectInternal(reason)
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
fulfill(value) {
if (this.state !== PENDING) return
this.state = FULFILLED
this.value = value
this.flush()
}
rejectInternal(reason) {
if (this.state !== PENDING) return
this.state = REJECTED
this.value = reason
this.flush()
}
resolveValue(value) {
if (value === this) {
this.rejectInternal(new TypeError('self resolution'))
return
}
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
this.fulfill(value)
return
}
let then
try {
then = value.then
} catch (error) {
this.rejectInternal(error)
return
}
if (typeof then !== 'function') {
this.fulfill(value)
return
}
queueMicrotask(() => {
let called = false
try {
Reflect.apply(then, value, [
nextValue => {
if (called) return
called = true
this.resolveValue(nextValue)
},
reason => {
if (called) return
called = true
this.rejectInternal(reason)
},
])
} catch (error) {
if (!called) this.rejectInternal(error)
}
})
}
then(onFulfilled, onRejected) {
return new MiniPromise((resolve, reject) => {
const reaction = () => {
queueMicrotask(() => {
const handler = this.state === FULFILLED ? onFulfilled : onRejected
if (typeof handler !== 'function') {
if (this.state === FULFILLED) resolve(this.value)
else reject(this.value)
return
}
try {
resolve(handler(this.value))
} catch (error) {
reject(error)
}
})
}
if (this.state === PENDING) this.reactions.push(reaction)
else reaction()
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
flush() {
const reactions = this.reactions
this.reactions = []
for (const reaction of reactions) reaction()
}
}
回调数组在 flush 时先被替换为空数组,避免长期持有闭包,也让执行过程中的新增反应按 settled 分支独立调度。
四、then 为什么一定返回新 Promise
若返回原对象,下面链条无法让每一段拥有独立结果:
const p = new MiniPromise(resolve => resolve(1))
const a = p.then(x => x + 1)
const b = p.then(x => x + 10)
a 应完成为 2,b 应完成为 11,二者不能改写 p。回调抛错时只拒绝对应的新 Promise;返回值则交给新 Promise 的 resolve 过程,因此返回 Promise 或 thenable 都能被吸收。
缺省成功处理器相当于 value => value,缺省失败处理器相当于 reason => { throw reason },所以值和错误都能沿链条穿透,直到遇到真正处理它的回调。
五、thenable 决议的四道防线
const hostile = {
then(resolve, reject) {
resolve(1)
reject(new Error('ignored'))
resolve(2)
},
}
可靠过程至少包含:
- 新 Promise 不能用自身完成,否则以
TypeError拒绝。 then属性只能读取一次,因为 getter 可能有副作用或抛错。- 调用 thenable 时绑定其自身为
this。 - 局部
called标志保证 resolve、reject、抛错三条路径只认第一次。
这里的 called 与构造器中的 alreadyResolved 解决不同问题:前者防恶意 thenable 多次回调,后者防 executor 同时调用 resolve 和 reject。
六、与原生 Promise 的差距
| 能力 | 本文实现 | 原生 Promise |
|---|---|---|
| 核心状态与链式 then | 支持 | 支持 |
| 通用 thenable 吸收 | 支持主要路径 | 规范完整支持 |
finally、静态组合方法 | 未实现 | 完整支持 |
子类与 Symbol.species | 未实现 | 有规范规则 |
| 未处理拒绝跟踪 | 未实现 | 由宿主环境跟踪 |
| 内部品牌和跨 realm 细节 | 未实现 | 引擎内部实现 |
另外,原生 resolve thenable 会创建专门的 Promise Job,循环 thenable、代理、realm 和错误顺序都有精细规则。本文适合解释机制和常规用例,不是替换浏览器原生 Promise 的生产 polyfill。
七、常见误区与追问
- 误区:调用
resolve就一定立刻变成 fulfilled。 resolve 到 pending Promise 或 thenable 时会锁定并等待其结果。 - 误区:pending 时异步,settled 后再
then就同步。 无论注册早晚,then回调都必须异步调度。 - 误区:只识别
value instanceof MiniPromise就够了。 决议过程要吸收任何拥有可调用then的对象或函数。 - 追问:为什么读取
then要放在 try/catch? 它可能是会抛错的 getter,读取失败应拒绝新 Promise。 - 追问:回调返回自身链条会怎样? 新 Promise 最终解析到自己,必须以
TypeError拒绝,不能无限等待。 - 追问:为什么不用
setTimeout调度回调? 它进入宏任务队列,和原生 Promise 微任务的相对顺序不同。 - 追问:
catch是独立机制吗? 不是,等价于then(undefined, onRejected)的便捷方法。
八、加强记忆
- 状态:pending 只能单向进入 fulfilled 或 rejected。
- 锁定:第一次 resolve/reject 消耗决议权,即使 thenable 尚未落定。
- 异步:executor 同步,
then反应进入微任务。 - 链式:每次
then返回新 Promise,结果互不污染。 - 吸收:读一次
then、防自身循环、防重复回调、捕获异常。 - 边界:教学类解释核心机制,不等同原生 Promise 的完整规范实现。