1. Promise 手写实现的核心价值
前端开发者在面试中经常会被要求手写Promise实现,这已经成为检验JavaScript异步编程能力的试金石。我见过太多候选人在这个问题上栽跟头,不是因为不会用Promise,而是对它的内部机制理解不够透彻。
手写Promise的难点不在于实现基本功能,而在于要完整复现其行为特性。比如微任务队列的处理、链式调用的实现、错误冒泡机制等。很多人在实现then方法时就卡住了,更不用说还要处理静态方法。
2. Promise 基础结构实现
2.1 构造函数与状态管理
我们先从最基础的Promise构造函数开始。一个Promise有三种状态:pending、fulfilled和rejected。这是整个实现的基石:
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) } } }这里有几个关键点需要注意:
- 状态一旦改变就不能再变(从pending到fulfilled/rejected)
- 使用队列存储回调函数是为了支持异步resolve的情况
- executor执行过程中抛出错误应该直接reject
2.2 then方法的实现
then方法是Promise的核心,也是面试中最常被考察的部分。它需要处理以下几种情况:
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 { 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 }这里有几个关键实现细节:
- 使用setTimeout模拟微任务队列
- 处理onFulfilled/onRejected不是函数的情况
- 实现链式调用需要返回新的Promise
- 需要处理回调函数中抛出异常的情况
2.3 resolvePromise函数
这是Promise A+规范要求的辅助函数,用于处理thenable对象和循环引用:
function resolvePromise(promise2, x, resolve, reject) { if (promise2 === x) { return reject(new TypeError('Chaining cycle detected for promise')) } if (x instanceof MyPromise) { x.then(resolve, reject) } else 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) } }3. 静态方法实现
3.1 Promise.resolve
这个静态方法创建一个立即resolve的Promise:
static resolve(value) { if (value instanceof MyPromise) { return value } return new MyPromise(resolve => { resolve(value) }) }需要注意处理参数本身就是Promise的情况。
3.2 Promise.reject
与resolve类似,创建一个立即reject的Promise:
static reject(reason) { return new MyPromise((_, reject) => { reject(reason) }) }3.3 Promise.all
这是最复杂的静态方法之一,需要处理多个Promise并行执行:
static all(promises) { return new MyPromise((resolve, reject) => { if (!Array.isArray(promises)) { return reject(new TypeError('Argument must be an array')) } const results = [] let count = 0 promises.forEach((promise, index) => { MyPromise.resolve(promise).then( value => { results[index] = value count++ if (count === promises.length) { resolve(results) } }, reject ) }) if (promises.length === 0) { resolve(results) } }) }关键点:
- 输入参数校验
- 处理空数组情况
- 保持结果顺序与输入一致
- 任何一个Promise reject就立即reject
3.4 Promise.race
返回第一个settled的Promise结果:
static race(promises) { return new MyPromise((resolve, reject) => { if (!Array.isArray(promises)) { return reject(new TypeError('Argument must be an array')) } promises.forEach(promise => { MyPromise.resolve(promise).then(resolve, reject) }) }) }4. 常见问题与调试技巧
4.1 微任务队列的实现
原生Promise使用微任务队列,而我们用setTimeout模拟的是宏任务。这在大多数面试场景下是可以接受的,但要知道两者的区别:
- 微任务:MutationObserver、process.nextTick
- 宏任务:setTimeout、setInterval
4.2 循环引用检测
这是很多手写实现会忽略的点:
const p = new MyPromise(resolve => { resolve(p) // 应该抛出TypeError })4.3 thenable对象处理
Promise A+规范要求处理thenable对象:
const thenable = { then: function(resolve) { resolve(42) } } MyPromise.resolve(thenable).then(console.log) // 应该输出424.4 错误处理边界
确保所有可能的错误都被捕获:
new MyPromise(() => { throw new Error('test') }).catch(console.log) // 应该捕获错误5. 测试用例验证
完整的Promise实现需要通过Promise A+测试套件。我们可以写一些基本测试:
// 基本功能测试 new MyPromise(resolve => resolve(1)) .then(val => val + 1) .then(console.log) // 2 // 异步测试 new MyPromise(resolve => setTimeout(() => resolve(2), 100)) .then(val => val * 2) .then(console.log) // 4 // 静态方法测试 MyPromise.all([ MyPromise.resolve(1), new MyPromise(resolve => setTimeout(() => resolve(2), 100)), 3 ]).then(console.log) // [1, 2, 3] // 错误处理测试 new MyPromise((_, reject) => reject('error')) .catch(err => { throw new Error(err) }) .catch(console.log) // Error: error6. 性能优化考虑
虽然面试中不常要求,但实际实现时可以考虑:
- 减少不必要的闭包创建
- 优化回调队列的数据结构
- 避免重复的状态检查
- 使用真正的微任务队列替代setTimeout
7. 与其他异步方案的对比
理解Promise与以下方案的异同:
- Callback:解决了回调地狱,但仍有then链的问题
- async/await:语法糖,底层还是Promise
- Generator:更底层的控制流管理
- RxJS:响应式编程,处理更复杂的异步场景
8. 面试中的高频问题
准备这些问题能帮你在面试中游刃有余:
- Promise的三种状态及其转换条件?
- then方法返回的是什么?
- 如何取消一个Promise?
- Promise.all和Promise.race的区别?
- 如何实现Promise超时控制?
- Promise的错误冒泡机制是怎样的?
- 为什么说Promise不能真正取消?