1. 为什么需要手写Promise
十年前我刚接触JavaScript时,回调地狱(callback hell)是最令人头疼的问题。直到ES6引入了Promise这个神器,才让异步代码的书写和阅读变得优雅。但很多开发者只是停留在会用的层面,今天我们就来解剖这只"黑盒子"。
Promise本质上是一个状态机,包含三种状态:
- Pending(等待中)
- Fulfilled(已成功)
- Rejected(已失败)
状态一旦改变就不可逆,这正是Promise可靠性的关键。比如当从Pending变为Fulfilled后,再调用resolve()也不会改变状态。
2. Promise核心结构实现
2.1 基础骨架搭建
我们先搭建一个最简单的Promise构造函数:
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)
}
}
}
这里有几个关键点需要注意:
- 状态初始为pending
- resolve/reject只有在pending状态才能改变状态
- 使用try-catch包裹executor,确保执行异常能被捕获
2.2 then方法实现
then方法是Promise的灵魂,我们先实现基础版本:
javascript复制then(onFulfilled, onRejected) {
if (this.state === 'fulfilled') {
onFulfilled(this.value)
} else if (this.state === 'rejected') {
onRejected(this.reason)
}
}
但这样实现有几个明显问题:
- 没有处理异步情况(当调用then时Promise还是pending状态)
- 没有实现链式调用
- 没有处理onFulfilled/onRejected不是函数的情况
3. 完善Promise功能
3.1 处理异步情况
我们需要增加回调队列来处理异步:
javascript复制constructor(executor) {
// ...原有代码
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
}
// reject同理
}
then(onFulfilled, onRejected) {
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
onFulfilled(this.value)
})
// reject同理
}
// ...其他情况处理
}
3.2 实现链式调用
Promise链式调用的关键在于then方法必须返回一个新的Promise:
javascript复制then(onFulfilled, onRejected) {
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)
}
}, 0)
}
// 其他状态处理类似
})
return promise2
}
这里有几个关键点:
- 使用setTimeout确保异步执行
- 通过resolvePromise处理返回值
- 错误处理要能传递到下一个Promise
3.3 resolvePromise实现
这是Promise最难的部分,需要处理各种返回值情况:
javascript复制function resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'))
}
// 如果x是Promise实例
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else {
// 普通值直接resolve
resolve(x)
}
}
4. 完整实现与测试
4.1 完整代码
整合前面的代码,我们的Promise实现如下:
javascript复制class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.reason = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
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)
}
}, 0)
} else if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
} else if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
catch(onRejected) {
return this.then(null, onRejected)
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'))
}
if (x instanceof MyPromise) {
x.then(resolve, reject)
} else {
resolve(x)
}
}
4.2 测试用例
让我们测试一下这个实现:
javascript复制// 基础功能测试
new MyPromise(resolve => {
setTimeout(() => resolve('success'), 1000)
}).then(res => {
console.log(res) // 1秒后输出'success'
})
// 链式调用测试
new MyPromise(resolve => resolve(1))
.then(res => {
console.log(res) // 1
return 2
})
.then(res => {
console.log(res) // 2
return new MyPromise(resolve => resolve(3))
})
.then(res => {
console.log(res) // 3
})
// 错误处理测试
new MyPromise((resolve, reject) => {
reject('error')
}).catch(err => {
console.log(err) // 'error'
})
5. 常见问题与解决方案
5.1 为什么需要setTimeout?
JavaScript是单线程的,使用setTimeout可以确保then回调总是异步执行,这符合Promise/A+规范。如果不这样做,同步代码和异步代码混用会导致执行顺序问题。
5.2 如何处理thenable对象?
更完整的resolvePromise实现应该处理thenable对象(带有then方法的对象):
javascript复制function resolvePromise(promise2, x, resolve, reject) {
// ...前面代码
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
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) return
reject(e)
}
} else {
resolve(x)
}
} else {
resolve(x)
}
}
5.3 如何实现Promise.all?
基于我们的实现,可以添加静态方法:
javascript复制static all(promises) {
return new MyPromise((resolve, reject) => {
const result = []
let count = 0
promises.forEach((p, i) => {
p.then(res => {
result[i] = res
count++
if (count === promises.length) {
resolve(result)
}
}, reject)
})
})
}
6. 性能优化与实践建议
6.1 内存管理
在实际实现中,当Promise状态改变后,回调数组应该被清空以避免内存泄漏:
javascript复制const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn())
this.onFulfilledCallbacks = [] // 清空数组
}
}
6.2 微任务优化
现代Promise实现使用微任务(microtask)而非setTimeout,可以使用queueMicrotask或MutationObserver来模拟:
javascript复制// 替换setTimeout
if (typeof queueMicrotask === 'function') {
queueMicrotask(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)
}
6.3 实际开发建议
- 总是返回新的Promise来实现链式调用
- 不要忘记错误处理,使用catch或then的第二个参数
- 避免在Promise中执行耗时同步操作
- 合理使用Promise.all等组合方法提高性能