1. 为什么我们需要手写Promise
2009年,Node.js的诞生让JavaScript正式进入服务端开发领域。随着异步编程场景的急剧增加,回调地狱(Callback Hell)成为每个JS开发者必须面对的噩梦。直到ES6(2015年)正式将Promise纳入标准,这种用then().catch()链式调用的异步方案才成为现代JavaScript的基石。
但你真的理解Promise吗?当我第一次尝试手写Promise实现时,才发现:
- 90%的开发者只会用
new Promise()却说不清它的状态机原理 - 大多数polyfill实现忽略了规范的细节要求
- 面试常问的"实现Promise"其实考察的是对异步编程本质的理解
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Promise核心规范解读
2.1 状态机的设计哲学
一个标准的Promise必须遵循这些规范:
-
三种状态:
- Pending(等待态)
- Fulfilled(成功态,必须有不可变的value)
- Rejected(失败态,必须有不可变的reason)
-
状态转换规则:
javascript复制new Promise((resolve, reject) => { resolve('success') // pending -> fulfilled // 后续再调用reject()无效 })
关键点:状态一旦变更就不可逆,这保证了Promise结果的确定性
2.2 then方法的双通道机制
.then方法的设计体现了Promise的核心价值:
javascript复制promise.then(
value => { /* 成功回调 */ },
reason => { /* 失败回调 */ }
)
实现时要处理这些边界情况:
- 当then的参数不是函数时,需要创建"穿透函数"
- 同一个promise可以多次调用then(发布订阅模式)
- 必须异步执行回调(即使当前已是完成状态)
3. 从零实现Promise
3.1 基础骨架搭建
我们先定义构造函数和状态管理:
javascript复制class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.reason = undefined
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
}
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
}
3.2 实现链式调用
真正的难点在于then方法的实现:
javascript复制then(onFulfilled, onRejected) {
// 参数校验(实现值穿透)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
// 返回新Promise实现链式调用
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
} else if (this.state === 'rejected') {
// 类似处理rejected状态...
} else {
// 处理pending状态(订阅回调)...
}
})
return promise2
}
3.3 解决循环引用问题
resolvePromise函数需要处理各种特殊情况:
javascript复制function resolvePromise(promise2, x, resolve, reject) {
// 禁止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'))
}
// 处理thenable对象
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let then
try {
then = x.then
} catch (e) {
return reject(e)
}
if (typeof then === 'function') {
// 处理Promise/A+兼容的thenable对象
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. 高级功能实现
4.1 静态方法实现
按照ES标准补充常用静态方法:
javascript复制static resolve(value) {
// 如果传入的就是Promise实例,直接返回
if (value instanceof MyPromise) return value
return new MyPromise(resolve => {
resolve(value)
})
}
static reject(reason) {
return new MyPromise((_, reject) => {
reject(reason)
})
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const result = []
let count = 0
const processResult = (index, value) => {
result[index] = value
if (++count === promises.length) {
resolve(result)
}
}
promises.forEach((p, i) => {
MyPromise.resolve(p).then(
val => processResult(i, val),
reject
)
})
})
}
4.2 微任务调度策略
现代Promise实现使用微任务队列(microtask),我们用queueMicrotask模拟:
javascript复制// 替换之前的setTimeout
if (this.state === 'fulfilled') {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
}
实际浏览器中,Promise回调是作为微任务执行的,这解释了为什么Promise.then比setTimeout优先级高
5. 从实现中学到的经验
5.1 常见调试陷阱
在实现过程中,这些坑让我记忆深刻:
- 循环引用检测:没有正确处理
promise2 === x的情况会导致无限递归 - 异步执行时机:用setTimeout模拟微任务时,测试用例的执行顺序会和原生Promise不同
- 错误处理边界:在then回调中抛出异常时,需要正确传递给下一个promise
5.2 性能优化方向
生产级的Promise实现还会考虑:
- 回调函数的缓存与复用
- 状态变更时的批量通知
- 更高效的任务调度策略
手写Promise的价值不在于造轮子,而是通过实现过程深入理解:
- 异步编程的本质
- 发布订阅模式的应用
- 链式调用的实现原理
- 错误冒泡的处理机制
当你下次使用Promise时,不妨想想背后的状态机是如何运转的——这才是前端工程师应有的思考深度。
