Promise 异步编程面试题全解析
一、核心要点速览
💡 核心考点
- 三种状态: Pending、Fulfilled、Rejected(状态不可逆)
- 链式调用: then/catch/finally 返回新 Promise
- 常用方法: all、race、allSettled、any
- 错误处理: 捕获异常、错误传播
二、Promise 基础:状态机模型
1. 三种状态与转换规律
Promise 本质上是一个状态机,它解决了异步操作中“何时结束”以及“结果是什么”的标准化描述。
- Pending (等待中): 初始状态,既没有成功,也没有失败。
- Fulfilled (已成功): 操作成功完成,调用
resolve()后进入此状态。 - Rejected (已失败): 操作失败,调用
reject()或抛出异常后进入此状态。
💡 状态特性:
- 原子性:一旦状态从 Pending 改变,就永远固定,不能再次改变。
- 不可逆:Fulfilled 和 Rejected 之间不能互相转换。
2. 基本使用演示
javascript
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const success = Math.random() > 0.5
success ? resolve('Data received') : reject('Network error')
}, 1000)
})
promise
.then(res => console.log('Success:', res))
.catch(err => console.error('Error:', err))
.finally(() => console.log('Operation finished'))三、链式调用:告别回调地狱
1. 为什么能链式调用?
then 方法总是返回一个新的 Promise。这使得我们可以将多个异步操作串联起来,像写同步代码一样书写异步逻辑。
2. 返回值规则(面试高频)
在 then 的回调函数中:
- 返回普通值:会被包装成
Promise.resolve(value)。 - 返回 Promise:
then返回的新 Promise 会“跟随”这个 Promise 的状态。 - 抛出异常:
then返回的新 Promise 会进入 Rejected 状态。
javascript
Promise.resolve(1)
.then(x => x + 1) // 返回 2,包装成 Promise
.then(x => { throw x }) // 抛出异常
.catch(err => err * 10) // 捕获异常,返回 20
.then(x => console.log(x)) // 输出 20四、并发控制:静态方法全家桶
在处理多个并发请求时,合理选择静态方法可以极大地提升性能和用户体验。
1. 核心方法详解
- Promise.all([p1, p2...]):
- 精髓:全成则成,一败则败。
- 场景:页面初始化需要同时获取用户信息、菜单列表和配置项。
- Promise.race([p1, p2...]):
- 精髓:快者为王。
- 场景:接口超时控制、多源响应竞争。
- Promise.allSettled([p1, p2...]):
- 精髓:不离不弃。
- 场景:批量上传文件,无论单个成功失败,都要知道最终所有结果。
- Promise.any([p1, p2...]):
- 精髓:一成即成。
- 场景:多 CDN 容灾切换,只要有一个节点响应即可。
五、深入:手写 Promise 实现机制
手写 Promise 是大厂面试的高频考点,它不仅考察你对 Promise 规范的理解,还考察了发布订阅模式和闭包的应用。
1. 核心设计思路:发布订阅模式
当我们在代码中执行 new Promise 并在内部进行异步操作(如 setTimeout)时,.then() 会先于异步结果执行。此时 Promise 处于 pending 状态,我们需要把 .then 中传入的回调函数存起来,等异步操作完成后再统一取出执行。
2. 标准手写实现 (面试版)
这个版本包含了 Promise/A+ 规范的核心逻辑,并添加了详细注释:
javascript
/**
* MyPromise 手写实现
* 核心逻辑:状态机 + 发布订阅 + 异步处理
*/
class MyPromise {
constructor(executor) {
// 1. 初始化状态
this.state = 'pending';
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
// 2. 存储回调函数队列 (订阅)
// 为什么是数组?因为同一个 promise 实例可以调用多次 .then()
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
// 3. 定义 resolve 函数
const resolve = (value) => {
// 只有 pending 状态可以转换 (状态不可逆)
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// 依次执行存储的回调 (发布)
this.onResolvedCallbacks.forEach(fn => fn());
}
};
// 4. 定义 reject 函数
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 5. 立即执行执行器,捕获异常
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
// then 方法实现
then(onFulfilled, onRejected) {
// 参数校验 (透传功能)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// 为了实现链式调用,必须返回一个新的 Promise
const promise2 = new MyPromise((resolve, reject) => {
// 核心难点:处理不同状态下的逻辑
// A. 如果状态已经是成功 (同步 resolve)
if (this.state === 'fulfilled') {
try {
const x = onFulfilled(this.value);
resolve(x); // 将回调结果传给 promise2
} catch (e) {
reject(e);
}
}
// B. 如果状态已经是失败 (同步 reject)
if (this.state === 'rejected') {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
}
// C. 如果状态还是 Pending (异步操作)
if (this.state === 'pending') {
// 核心:此时不能执行回调,而是把执行逻辑存入队列
this.onResolvedCallbacks.push(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
});
}
});
return promise2;
}
}
// 补充静态方法:Promise.all
MyPromise.all = function(promises) {
return new MyPromise((resolve, reject) => {
let result = [];
let count = 0;
promises.forEach((p, index) => {
// 确保是 Promise
MyPromise.resolve(p).then(res => {
result[index] = res;
count++;
if (count === promises.length) resolve(result);
}, err => {
reject(err); // 只要有一个失败,整体就失败
});
});
});
};
// 补充静态方法:Promise.resolve
MyPromise.resolve = function(val) {
return new MyPromise((resolve) => resolve(val));
};3. 手写实现中的三个关键难点
- 为什么需要回调数组? 同一个 Promise 实例可以多次调用
.then()。如果不使用数组,后面的.then回调会覆盖前面的。 - 为什么
.then要返回新 Promise? 这是实现链式调用的基础。原生的 Promise 规范规定,每个.then都会产生一个新的 Promise,而不是返回this。 - 什么是“透传”? 如果
.then()没有传回调函数(例如promise.then().then(res => ...)),我们需要保证结果能一直传到最后。代码中通过onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;实现了这一点。
六、高频面试题
1. Promise 解决了什么问题?
回答:
- 回调地狱 (Callback Hell):通过链式调用解决了多层嵌套导致的代码难以维护和调试的问题。
- 信任问题:回调函数由第三方库调用时可能存在调用多次、不调用等问题,Promise 状态一旦改变即固定的特性保证了结果的可靠性。
- 错误处理:通过
catch实现了错误的冒泡捕获,比传统的回调错误处理更优雅。
2. Promise.all 中如果有一个失败了,其他请求会停止吗?
回答: 不会停止。Promise.all 的“短路”机制只是意味着它会立即进入 Rejected 状态,但已经发出的异步操作(如 HTTP 请求)无法撤回,它们仍会继续执行直到完成,只是其结果会被 Promise.all 忽略。
3. 如何实现 Promise 的超时控制?
回答: 使用 Promise.race()。将业务请求的 Promise 与一个在指定时间后自动 Reject 的 setTimeout Promise 放入数组中,谁快就取谁的结果。
javascript
function timeoutWrapper(p, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([p, timeout]);
}4. .then() 的第二个参数和 .catch() 有什么区别?
回答:
.then(success, fail):第二个参数只能捕获当前 Promise 的错误,无法捕获success回调函数中抛出的错误。.catch():可以捕获链条中之前任何位置抛出的错误,包括then中成功回调产生的错误。推荐使用catch进行统一处理。
七、面试答题模板:如何优雅地介绍 Promise?
“Promise 是 ES6 引入的异步编程解决方案,它本质上是一个容器,保存着某个未来才会结束的事件的结果。”
- 核心机制:它有三种状态(Pending、Fulfilled、Rejected),状态转换由
resolve和reject触发,且一旦改变即不可逆。- 解决痛点:它通过链式调用解决了“回调地狱”,让异步代码拥有了同步的逻辑感;同时提供了
all、race等方法方便处理并发。- 实战经验:在项目中我常配合
async/await使用,它是 Promise 的语法糖,让代码更简洁。在处理并发请求时,我会根据业务需求灵活选择allSettled(如批量上传)或any(如多源备份)。
八、记忆口诀
Promise 歌诀:
Promise 有三种态,
pending fulfilled rejected。
状态只能变一次,
链式调用解千愁。
then 接成功 catch 错,
finally 总是被执行。
all 要全都成功,
race 比谁快第一!
allSettled 等全部,
any 要一个成功。
错误传播向下走,
链尾记得加 catch!九、推荐资源
十、总结一句话
- Promise: 状态机 + 链式调用 = 异步编程标准化 ⚡
- all/race: 聚合并发 + 灵活控制 = 并发请求利器 🚀
- 错误处理: catch 捕获 + 错误传播 = 健壮的异步代码 ✓