1. 为什么我们需要手写Promise
2009年,当Node.js首次将JavaScript带入服务端开发领域时,回调地狱(Callback Hell)问题就成为了每个JS开发者心中的痛。层层嵌套的回调函数不仅让代码难以阅读,更给错误处理和流程控制带来了巨大挑战。直到ES6正式推出Promise规范,这种局面才得到根本性改变。
我在2016年接手一个遗留项目时,曾见过这样的代码:
fs.readFile('config.json', function(err, data) { if (err) return console.error(err); parseXML(data, function(err, result) { if (err) return console.error(err); db.query('SELECT...', function(err, rows) { if (err) return console.error(err); // 更多嵌套... }); }); });这段代码暴露了回调模式的三个致命缺陷:
- 错误处理重复且分散
- 执行流程难以追踪
- 异步操作无法并行管理
2. Promise核心机制解析
2.1 状态机的本质
Promise本质上是一个状态机,包含三个不可逆的状态转换:
- pending → fulfilled(通过resolve)
- pending → rejected(通过reject)
我曾用简单的对象模拟过这个机制:
const fakePromise = { state: 'pending', value: null, reason: null, resolve: function(value) { if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = value; }, reject: function(reason) { if (this.state !== 'pending') return; this.state = 'rejected'; this.reason = reason; } }2.2 微任务的奥秘
Promise的回调执行时机是个容易误解的点。在Chrome中实测以下代码:
console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 0); Promise.resolve().then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); }); console.log('script end');输出顺序将是:
script start script end promise1 promise2 setTimeout这是因为Promise回调属于微任务(microtask),而setTimeout是宏任务(macrotask)。事件循环会优先清空微任务队列。
3. 完整实现手写Promise
3.1 基础框架搭建
我们先构建最简化的Promise骨架:
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; const resolve = (value) => { if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = value; }; const reject = (reason) => { if (this.state !== 'pending') return; this.state = 'rejected'; this.reason = reason; }; try { executor(resolve, reject); } catch (err) { reject(err); } } }这个基础版本已经可以处理同步操作:
const p = new MyPromise((resolve) => { resolve(42); });3.2 then方法实现
then方法是Promise最复杂的部分,需要考虑:
- 状态未完成时的回调暂存
- 链式调用支持
- 值穿透处理
完整实现:
then(onFulfilled, onRejected) { // 值穿透处理 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v; onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }; const promise2 = new MyPromise((resolve, reject) => { const handle = () => { queueMicrotask(() => { try { const x = this.state === 'fulfilled' ? onFulfilled(this.value) : onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (err) { reject(err); } }); }; if (this.state !== 'pending') { handle(); } else { this.onFulfilledCallbacks.push(() => handle()); this.onRejectedCallbacks.push(() => handle()); } }); return promise2; }3.3 Promise解决过程
这是符合Promises/A+规范的核心:
function resolvePromise(promise2, x, resolve, reject) { // 防止循环引用 if (x === promise2) { return reject(new TypeError('Chaining cycle detected')); } // 处理thenable对象 if ((typeof x === 'object' && x !== null) || typeof x === 'function') { let then; try { then = x.then; } catch (err) { return reject(err); } 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 (err) { if (!called) reject(err); } } else { resolve(x); } } else { resolve(x); } }4. 高级功能实现
4.1 静态方法实现
Promise.resolve的polyfill:
static resolve(value) { if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); }Promise.reject的简单实现:
static reject(reason) { return new MyPromise((_, reject) => reject(reason)); }4.2 all/race方法
Promise.all的典型实现:
static all(promises) { return new MyPromise((resolve, reject) => { const results = []; let count = 0; const processResult = (i, val) => { results[i] = val; if (++count === promises.length) resolve(results); }; promises.forEach((p, i) => { MyPromise.resolve(p).then( val => processResult(i, val), reject ); }); }); }Promise.race的实现更简单:
static race(promises) { return new MyPromise((resolve, reject) => { promises.forEach(p => { MyPromise.resolve(p).then(resolve, reject); }); }); }5. 常见问题与调试技巧
5.1 异步测试陷阱
测试Promise时常见的一个错误是忘记测试是异步的:
// 错误示例 it('should resolve', () => { const p = new MyPromise(resolve => resolve(42)); p.then(val => { expect(val).toBe(42); // 这行可能不会执行 }); }); // 正确写法 it('should resolve', done => { const p = new MyPromise(resolve => resolve(42)); p.then(val => { expect(val).toBe(42); done(); }); });5.2 内存泄漏排查
在实现过程中,我曾遇到内存泄漏问题。通过Chrome DevTools的Memory面板发现,未完成的Promise会保持对回调函数的引用。解决方案是:
// 在适当的时候清理回调数组 this.onFulfilledCallbacks = null; this.onRejectedCallbacks = null;5.3 性能优化点
- 使用微任务队列替代setTimeout:
// 优先使用现代API if (typeof queueMicrotask === 'function') { queueMicrotask(fn); } else { Promise.resolve().then(fn); }- 避免不必要的Promise封装:
// 不推荐 new Promise(resolve => resolve(someValue)); // 推荐 Promise.resolve(someValue);6. 从实现中学到的经验
在完整实现Promise的过程中,最深刻的体会是错误处理的重要性。最初版本我忽略了这两个关键点:
- executor中的同步错误必须捕获:
try { executor(resolve, reject); } catch (err) { reject(err); }- then方法中的回调执行也需要try-catch:
try { const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (err) { reject(err); }另一个收获是对异步编程本质的理解。通过实现Promise,我真正明白了:
- 事件循环中微任务的优先级
- 状态不可变性的重要性
- 链式调用的实现原理
这些知识在调试复杂异步流程时非常有用。比如当遇到"Promise似乎没有执行"的情况时,现在我会首先检查:
- 是否忘记return新的Promise
- 是否有未捕获的异常
- 微任务队列是否被阻塞