1. 为什么Promise是前端面试的必考题目
Promise作为JavaScript异步编程的核心解决方案,几乎出现在所有现代前端项目中。根据2023年Stack Overflow开发者调查,超过87%的JavaScript项目使用了Promise相关API。面试官之所以钟爱这个考点,主要基于三个原因:
首先,Promise体现了开发者对JavaScript事件循环机制的理解深度。一个能完整实现Promise的候选人,必然清楚microtask队列、执行上下文等底层概念。我在面试中经常发现,很多自称"精通异步编程"的候选人,在被要求手写Promise时却无法正确处理then方法的链式调用。
其次,Promise的实现涉及众多JS核心特性。包括但不限于:
- 闭包的应用(维护内部状态)
- this绑定问题(回调函数的执行上下文)
- 原型链继承(Promise方法的共享)
- 错误冒泡机制(catch的实现)
最后,考察Promise能快速检验编码规范。一个健壮的Promise实现需要考虑边界情况处理、状态不可变性、类型检查等工程化细节。我曾见过一个候选人用20行代码实现了基本功能,但完全忽略了错误处理,这种实现在实际项目中就是定时炸弹。
2. Promise核心机制解析
2.1 状态机的本质
Promise本质上是一个状态机,包含三个互斥状态:
- PENDING(等待)
- FULFILLED(成功)
- REJECTED(失败)
用TypeScript描述状态转移如下:
type Status = 'PENDING' | 'FULFILLED' | 'REJECTED'; class MyPromise { private status: Status = 'PENDING'; private value: any; private reason: any; constructor(executor: ( resolve: (value: any) => void, reject: (reason: any) => void ) => void) { const resolve = (value: any) => { if (this.status === 'PENDING') { this.status = 'FULFILLED'; this.value = value; } }; const reject = (reason: any) => { if (this.status === 'PENDING') { this.status = 'REJECTED'; this.reason = reason; } }; try { executor(resolve, reject); } catch (e) { reject(e); } } }关键设计点:
- 状态只能从PENDING向FULFILLED/REJECTED单向变化
- 使用try-catch包裹executor执行,确保同步错误能被捕获
- resolve/reject内部需要检查当前状态,避免重复变更
2.2 微任务队列的实现
根据Promise/A+规范,then方法的回调必须异步执行。在现代浏览器中,这通常通过微任务(microtask)实现。我们可以用以下方式模拟:
// 简易微任务队列实现 const microTask = (callback: Function) => { if (typeof queueMicrotask === 'function') { queueMicrotask(callback); } else if (typeof MutationObserver === 'function') { const observer = new MutationObserver(callback); const textNode = document.createTextNode(''); observer.observe(textNode, { characterData: true }); textNode.data = '1'; } else { setTimeout(callback, 0); } };实际项目中更推荐直接使用queueMicrotask API。需要注意:
- 不要用setTimeout模拟,它属于宏任务(macrotask)
- MutationObserver回调确实是微任务,但存在兼容性问题
- Node环境可以用process.nextTick
3. 完整Promise实现
3.1 基础版本实现
先实现最基础的then方法:
class MyPromise { // ...省略之前代码... then(onFulfilled?: (value: any) => any, onRejected?: (reason: any) => any) { const promise2 = new MyPromise((resolve, reject) => { const handleFulfilled = () => { microTask(() => { try { if (typeof onFulfilled === 'function') { const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } else { resolve(this.value); } } catch (e) { reject(e); } }); }; const handleRejected = () => { microTask(() => { try { if (typeof onRejected === 'function') { const x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } else { reject(this.reason); } } catch (e) { reject(e); } }); }; if (this.status === 'FULFILLED') { handleFulfilled(); } else if (this.status === 'REJECTED') { handleRejected(); } else { // PENDING状态暂存回调 this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); return promise2; } }关键点解析:
- then方法必须返回新的Promise实例(promise2)
- 根据当前状态决定立即执行还是暂存回调
- 使用resolvePromise处理返回值(可能是thenable对象)
- 所有回调必须异步执行(通过microTask)
3.2 resolvePromise的实现
这是Promise实现中最复杂的部分:
function resolvePromise( promise2: MyPromise, x: any, resolve: (value: any) => void, reject: (reason: any) => void ) { if (promise2 === x) { return reject(new TypeError('Chaining cycle detected')); } 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) { reject(e); } } } else { resolve(x); } } else { resolve(x); } }这段代码处理了各种特殊情况:
- 避免循环引用(promise2 === x)
- 处理thenable对象(如第三方Promise实现)
- 防止多次调用(called标志位)
- 保持与原生Promise的行为一致
4. Promise静态方法实现
4.1 Promise.resolve / Promise.reject
class MyPromise { // ...省略之前代码... static resolve(value: any) { if (value instanceof MyPromise) { return value; } return new MyPromise((resolve) => resolve(value)); } static reject(reason: any) { return new MyPromise((_, reject) => reject(reason)); } }4.2 Promise.all
class MyPromise { // ...省略之前代码... static all(promises: Array<any>) { return new MyPromise((resolve, reject) => { const result: Array<any> = []; let count = 0; const processResult = (index: number, value: any) => { result[index] = value; if (++count === promises.length) { resolve(result); } }; promises.forEach((p, i) => { MyPromise.resolve(p).then( (value) => processResult(i, value), reject ); }); }); } }注意事项:
- 输入不一定是Promise数组,需要用Promise.resolve包装
- 保持结果的顺序与输入一致
- 任一Promise失败立即reject
4.3 Promise.race
class MyPromise { // ...省略之前代码... static race(promises: Array<any>) { return new MyPromise((resolve, reject) => { promises.forEach((p) => { MyPromise.resolve(p).then(resolve, reject); }); }); } }5. 常见问题与调试技巧
5.1 为什么我的then回调没有执行?
可能原因:
- 忘记返回新的Promise(链式调用中断)
- 在前一个Promise中未调用resolve/reject(状态一直为PENDING)
- 在executor中抛出异常但未处理
调试建议:
// 添加状态日志 console.log('Current state:', this.status);5.2 如何测试Promise实现的正确性?
推荐使用promises-aplus-tests测试套件:
- 安装测试工具:
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 MyPromise.js5.3 性能优化建议
- 避免不必要的Promise封装:
// 不推荐 new Promise(resolve => resolve(syncValue)); // 推荐 Promise.resolve(syncValue);- 合理控制并发:
// 使用Promise.all时控制并发数 async function parallel(tasks, concurrency) { const results = []; for (let i = 0; i < tasks.length; i += concurrency) { const chunk = tasks.slice(i, i + concurrency); results.push(...await Promise.all(chunk)); } return results; }6. 进阶实现:取消功能
原生Promise不支持取消,但我们可以扩展实现:
class CancelablePromise extends MyPromise { private _canceled = false; cancel() { this._canceled = true; } then(onFulfilled, onRejected) { const wrappedOnFulfilled = (value) => { if (!this._canceled) { return onFulfilled?.(value); } }; return super.then(wrappedOnFulfilled, onRejected); } }使用场景:
const p = new CancelablePromise((resolve) => { setTimeout(() => resolve('done'), 1000); }); p.then(console.log); p.cancel(); // 不会输出'done'注意事项:
- 这只是表面上的取消,异步操作仍在后台执行
- 真正的取消需要配合AbortController等机制