1. 为什么需要手写Promise
在JavaScript的世界里,异步编程一直是个绕不开的话题。记得我刚入行时,面对层层嵌套的回调函数(callback hell),代码缩进都快超出屏幕右边距了。直到ES6引入了Promise,才让异步代码有了线性表达的可能。
但仅仅会使用Promise还远远不够。去年面试一个三年经验的候选人时,我让他解释Promise的then方法如何实现链式调用,对方支支吾吾说不出所以然。这正是促使我深入研究Promise实现原理的契机——只有亲手实现过,才能真正理解这个每天都要打交道的工具。
2. Promise的核心机制剖析
2.1 状态机的本质
Promise本质上是一个状态机,它有三种状态:
- PENDING(等待)
- FULFILLED(成功)
- REJECTED(失败)
状态一旦改变就不可逆。这个特性用代码表示就是:
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) } } }2.2 微任务队列的玄机
Promise的回调属于微任务(microtask),这决定了它的执行时机。在浏览器中我们常用queueMicrotask或MutationObserver模拟,Node环境下可以用process.nextTick:
const asyncTask = (fn) => { 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) } }注意:虽然setTimeout也能实现异步,但它属于宏任务(macrotask),与原生Promise的行为有差异,只应作为降级方案。
3. Then方法的实现艺术
3.1 基础链式调用
then方法需要返回新的Promise,这是链式调用的关键:
then(onFulfilled, onRejected) { const promise2 = new MyPromise((resolve, reject) => { if (this.state === 'FULFILLED') { asyncTask(() => { try { const x = onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) } // 其他状态处理... }) return promise2 }3.2 值穿透的陷阱
当then的参数不是函数时,要实现"值穿透"特性:
then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err } // ...其余代码不变 }4. 解决程序(resolvePromise)的复杂逻辑
这是整个实现中最复杂的部分,需要处理各种边界情况:
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') { 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. 其他原型方法的实现
5.1 catch方法
catch实际上是then的语法糖:
catch(onRejected) { return this.then(null, onRejected) }5.2 finally方法
finally的特点是不影响Promise链的值传递:
finally(callback) { return this.then( value => MyPromise.resolve(callback()).then(() => value), reason => MyPromise.resolve(callback()).then(() => { throw reason }) ) }6. 静态方法的实现
6.1 Promise.resolve
需要处理thenable对象的递归解析:
static resolve(value) { if (value instanceof MyPromise) return value return new MyPromise(resolve => { resolve(value) }) }6.2 Promise.reject
与resolve不同,reject不会展开thenable对象:
static reject(reason) { return new MyPromise((_, reject) => { reject(reason) }) }6.3 Promise.all
需要处理空数组等边界情况:
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 if (promises.length === 0) { return resolve(results) } promises.forEach((p, i) => { MyPromise.resolve(p).then( value => { results[i] = value if (++count === promises.length) { resolve(results) } }, reject ) }) }) }7. 测试你的Promise实现
7.1 使用Promises/A+测试套件
安装测试工具:
npm install promises-aplus-tests -D添加适配器:
MyPromise.deferred = function() { const dfd = {} dfd.promise = new MyPromise((resolve, reject) => { dfd.resolve = resolve dfd.reject = reject }) return dfd }运行测试:
npx promises-aplus-tests your-promise.js7.2 常见边界测试用例
// 1. 同步resolve new MyPromise(resolve => resolve(1)).then(console.log) // 2. 异步resolve new MyPromise(resolve => { setTimeout(() => resolve(2), 100) }).then(console.log) // 3. thenable对象 new MyPromise(resolve => { resolve({ then(resolve) { resolve(3) } }) }).then(console.log) // 4. 链式调用 MyPromise.resolve(4) .then(v => v * 2) .then(console.log)8. 性能优化实践
8.1 避免不必要的微任务
在状态已确定时,可以优化then方法的执行:
then(onFulfilled, onRejected) { // 状态已确定时直接执行,不进入微任务队列 if (this.state === 'FULFILLED') { const promise2 = new MyPromise((resolve, reject) => { try { const x = onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) } catch (e) { reject(e) } }) return promise2 } // ...其他状态处理 }8.2 内存泄漏防范
在大型应用中要注意:
- 及时处理被拒绝的Promise(避免未捕获的rejection)
- 避免在Promise闭包中保存大对象
- 对长时间pending的Promise设置超时:
function timeoutPromise(promise, ms) { return MyPromise.race([ promise, new MyPromise((_, reject) => { setTimeout(() => reject(new Error('timeout')), ms) }) ]) }手写Promise的过程就像在解构一个精密的瑞士手表,每个齿轮的咬合都需要精确到位。当最后通过所有测试用例时,那种对异步编程豁然开朗的感觉,比任何文档教程都来得深刻。建议每个JS开发者都至少完整实现一次Promise,这会是理解异步编程最好的投资。