1. 为什么需要手写Promise
十年前我刚接触JavaScript回调地狱时,曾经用嵌套三层的回调函数实现过文件读取流程。当看到同事用Promise重构后的代码时,那种链式调用的优雅让我意识到异步编程的新范式正在形成。如今Promise已成为现代JavaScript的基石,但很多开发者仍停留在"会用但不懂原理"的阶段。
手写Promise实现的价值在于:
- 彻底理解then/catch的链式调用机制
- 掌握异步状态管理的核心设计模式
- 能在面试中从容应对原理性追问
- 为理解更复杂的async/await打下基础
2. Promise核心机制解析
2.1 状态机的设计
Promise本质上是一个状态机,包含三个互斥状态:
javascript复制const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
状态转换规则:
- 初始状态必须是pending
- 只能从pending变为fulfilled或rejected
- 状态变更后不可逆转
2.2 执行器函数的双向控制
构造函数接收的executor函数暗藏玄机:
javascript复制constructor(executor) {
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
关键点:
- resolve/reject需要在构造函数内定义
- 必须添加状态保护(if pending判断)
- 需要try-catch包裹executor执行
3. Then方法的实现艺术
3.1 基础链式调用实现
javascript复制then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}
// 其他状态处理...
})
return promise2
}
3.2 异步回调队列的处理
pending状态时需要建立回调队列:
javascript复制this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
// 在then方法中:
else if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
4. 解决程序(resolvePromise)的奥秘
4.1 处理thenable对象
javascript复制function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('循环引用'))
}
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else if (typeof x === 'object' || typeof x === 'function') {
if (x === null) return resolve(x)
let then
try {
then = x.then
} catch (e) {
return reject(e)
}
if (typeof then === 'function') {
let called = false
try {
then.call(x,
y => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
},
r => {
if (called) return
called = true
reject(r)
}
)
} catch (e) {
if (!called) reject(e)
}
} else {
resolve(x)
}
} else {
resolve(x)
}
}
4.2 循环引用检测
在resolvePromise开头必须检查:
javascript复制if (promise2 === x) {
reject(new TypeError('Chaining cycle detected'))
}
5. 完整实现与测试用例
5.1 完整代码结构
javascript复制class MyPromise {
constructor(executor) {
// 状态定义
this.status = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
// 执行器函数
const resolve = (value) => {
// 状态变更逻辑
}
const reject = (reason) => {
// 状态变更逻辑
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
// then方法实现
}
catch(onRejected) {
return this.then(null, onRejected)
}
static resolve(value) {
// 静态方法实现
}
static reject(reason) {
// 静态方法实现
}
}
5.2 关键测试场景
javascript复制// 基础功能测试
new MyPromise(resolve => resolve(1))
.then(console.log) // 1
// 异步测试
new MyPromise(resolve =>
setTimeout(() => resolve(2), 1000)
).then(console.log)
// 链式调用测试
MyPromise.resolve(3)
.then(v => v * 2)
.then(console.log) // 6
// 错误处理测试
MyPromise.reject(new Error('fail'))
.catch(e => console.error(e.message))
6. 常见问题与调试技巧
6.1 典型错误排查
-
回调未执行:
- 检查resolve/reject是否在pending状态下触发
- 确认setTimeout是否正确包裹回调
-
状态混乱:
- 确保状态变更前检查currentStatus === PENDING
- 使用Object.freeze冻结状态常量
-
内存泄漏:
- 回调数组在触发后应该清空
- 避免在闭包中保留不必要引用
6.2 性能优化建议
- 使用微任务队列替代setTimeout:
javascript复制const microTask = fn => {
if (typeof process === 'object' && process.nextTick) {
process.nextTick(fn)
} else if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(fn)
const textNode = document.createTextNode('')
observer.observe(textNode, { characterData: true })
textNode.data = '1'
} else {
setTimeout(fn, 0)
}
}
- 避免不必要的Promise包装:
javascript复制// 不推荐
then(v => new Promise(resolve => resolve(v + 1)))
// 推荐
then(v => v + 1)
7. 扩展实现与进阶思考
7.1 Promise静态方法实现
javascript复制static all(promises) {
return new MyPromise((resolve, reject) => {
const result = []
let count = 0
promises.forEach((p, i) => {
MyPromise.resolve(p).then(
value => {
result[i] = value
if (++count === promises.length) resolve(result)
},
reject
)
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(p => {
MyPromise.resolve(p).then(resolve, reject)
})
})
}
7.2 取消功能扩展
javascript复制class CancelablePromise extends MyPromise {
constructor(executor) {
super(executor)
this._isCanceled = false
}
cancel() {
this._isCanceled = true
}
then(onFulfilled, onRejected) {
return super.then(
value => this._isCanceled ? undefined : onFulfilled(value),
reason => this._isCanceled ? undefined : onRejected(reason)
)
}
}
在实现过程中最让我意外的是then方法返回新Promise时产生的循环引用问题。第一次测试时遇到"Maximum call stack size exceeded"错误,通过添加promise2 === x的检测才明白规范中这个边缘case的重要性。这也让我更深刻理解了Promise/A+规范每个条款背后的实际意义。