13. 说说你对Promise的理解?Promise.all和Promise.race的区别?
理解 Promise 的单次决议、thenable 接管、微任务入队、链式错误传播与并发组合边界。
问题解析
Promise是JavaScript异步编程的重要解决方案,理解Promise的原理、状态变化以及静态方法的使用是前端面试中的高频考点。本题主要考察对Promise核心概念的理解以及Promise.all和Promise.race的区别和应用场景。
核心概念
什么是Promise
Promise是ES6引入的异步编程解决方案,它是一个对象,代表了一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:
- pending(进行中):初始状态,既未完成也未拒绝
- fulfilled(已成功):操作成功完成
- rejected(已失败):操作失败
Promise的特点
- 状态一旦改变,就不会再变:Promise对象的状态改变只有两种可能:从pending变为fulfilled,或者从pending变为rejected
- 链式调用:通过then方法可以进行链式调用,解决回调地狱问题
- 错误捕获:通过catch方法可以统一捕获错误
详细解答
Promise基本用法
const promise = new Promise((resolve, reject) => {
const shouldSucceed = true;
setTimeout(() => {
if (shouldSucceed) {
resolve('完成');
} else {
reject(new Error('操作失败'));
}
}, 1000);
});
promise
.then(value => {
// 成功时的回调
console.log(value);
})
.catch(error => {
// 失败时的回调
console.error(error);
})
.finally(() => {
// 无论成功失败都会执行
console.log('完成');
});
Promise.all
Promise.all用于将多个Promise实例包装成一个新的Promise实例。
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(values => {
console.log(values); // [1, 2, 3]
})
.catch(error => {
console.error(error);
});
特点:
- 所有Promise都成功时,返回的Promise才会成功,结果是一个数组,按传入顺序排列
- 只要有一个Promise失败,整个Promise.all就失败,返回第一个失败的Promise的原因
Promise.race
Promise.race同样是将多个Promise实例包装成一个新的Promise实例,但只要有一个实例率先改变状态,整个Promise的状态就跟着改变。
const p1 = new Promise((resolve) => {
setTimeout(resolve, 500, 'one');
});
const p2 = new Promise((resolve) => {
setTimeout(resolve, 1000, 'two');
});
Promise.race([p1, p2])
.then(value => {
console.log(value); // 'one',因为p1先完成
});
特点:
- 哪个Promise先完成(无论成功或失败),就采用哪个Promise的结果
- 常用于设置超时处理
Promise.all与Promise.race的区别
| 特性 | Promise.all | Promise.race |
|---|---|---|
| 触发条件 | 所有Promise完成 | 第一个Promise完成 |
| 成功结果 | 所有Promise结果的数组 | 第一个完成的Promise结果 |
| 失败条件 | 任意一个Promise失败 | 第一个完成的Promise失败 |
| 应用场景 | 并行执行多个异步操作 | 竞速、超时控制 |
深入理解
单次决议解决什么问题
回调式接口把后续函数交给提供者,调用方需要担心回调被重复调用、过早同步调用或忘记调用。Promise 通过单次决议和异步 reaction 提供统一消费契约:提供者通知结果,调用方选择如何链接后续操作。同一 Promise 的每次 then 注册对应的处理器最多运行一次,重复 resolve/reject 不会重复触发它。
这不等于业务操作只执行一次。若第三方在调用 resolve 前已经重复扣款,Promise 无法撤销这些副作用;写请求仍需服务端幂等控制。永不调用 resolve/reject 的提供者也仍可让 Promise 一直 pending。
resolve 不一定立即 fulfilled
resolved 表示结果已被锁定,可以是采用另一个尚未完成的 Promise;fulfilled/rejected 才是最终状态。例如先 resolve(pendingPromise) 再 reject(error),后者会被忽略,但外层仍可能暂时 pending。不能只用“状态是否仍是 pending”判断能否再次决议。
Promise.resolve(value) 与 then 返回值的吸收遵循相同的关键规则:
| 值 | 如何处理 |
|---|---|
普通值,或 then 不可调用的对象 |
以该值成功完成 |
同构造器的原生 Promise(Promise.resolve) |
通常直接返回该实例;不要推广为任意子类/跨 realm 对象都保留引用 |
带可调用 then 的对象或函数 |
读取 then,通过 job 调用它并传入 resolve/reject;采用回调给出的结果 |
读取 then 抛错,或在尚未决议时调用 then 抛错 |
拒绝 Promise |
then 可来自自身属性或原型链;不是采用 then 方法的 return 值。普通函数返回值的规则适用于 then(onFulfilled) 中的处理器,别把两层 then 混为一谈。
const thenable = {
then(resolve, reject) {
resolve(42);
reject(new Error('后续决议被忽略'));
return 99; // 不会成为结果
}
};
Promise.resolve(thenable).then(console.log); // 42
Promise.resolve({ then: 1 }).then(console.log); // { then: 1 }
reaction 何时进入微任务队列
new Promise(executor) 同步执行 executor;resolve() 不会同步执行已注册的 then 回调。pending 时调用 then 先保存 reaction;Promise 最终完成后才将对应 reaction 入队。对已经完成的 Promise 调用 then,则在注册时安排 reaction job。没有处理器的 resolve(普通值) 不会凭空生成一个用户回调任务;吸收 thenable 则还可能安排专用 job。
const order = [];
let finish;
const pending = new Promise(resolve => { finish = resolve; });
pending.then(() => order.push('pending reaction'));
queueMicrotask(() => order.push('先入队'));
finish();
Promise.resolve().then(() => order.push('settled reaction'));
order.push('同步');
setTimeout(() => console.log(order), 0);
// ['同步', '先入队', 'pending reaction', 'settled reaction']
Promise的链式调用
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error(error));
每次 then/catch/finally 都返回新 Promise。then 处理器返回普通值会使下游成功,抛错会使下游拒绝,返回 Promise/thenable 则等待其结果。catch 是 then(undefined, onRejected);在 catch 中只打印而不重新抛出,会把下游转为 fulfilled(通常值为 undefined)。
finally 有不同契约:回调不接收结果,普通返回值不替换原成功值/失败原因;若抛错或返回拒绝的 Promise,才用新的失败覆盖原结果。不要把 finally(() => fallback) 当成错误恢复,也不要丢弃可能拒绝的 p.finally(cleanup) 返回链。
Promise.resolve(1).finally(() => 99).then(console.log); // 1
Promise.reject('失败').catch(() => '降级值').then(console.log); // 降级值
Promise.resolve(1).finally(() => { throw new Error('清理失败'); })
.catch(error => console.log(error.message)); // 清理失败
组合 API 接受 iterable,元素也可以是普通值/thenable,结果位置遵循输入顺序。all([])、allSettled([]) 成功得到 [],race([]) 永远 pending;Promise.reject(thenable) 直接把对象作为拒绝原因,不吸收它。all 的首错或 race 的胜出都不会自动取消其他操作。更多组合示例复用 ES6 Promise API。
手写 Promise 的最小实现范围
面试中的“手写 Promise”通常不是要求完整复现规范,而是考察能否说明以下核心职责:
- 保存
pending、fulfilled、rejected三种状态,以及对应的结果值或失败原因。 - 首次
resolve/reject锁定结果;即使仍在等待 thenable,后续决议也无效。 then在 Promise 未完成时缓存成功与失败回调,完成后再异步依次执行。then返回一个新的 Promise;返回值需要继续传递,返回 Promise 时需要等待它的状态。
可以先从状态、回调队列和链式返回值三部分拆开实现,再补齐异常捕获与 thenable 处理等 Promise/A+ 细节。
Promise.all的实际应用
// 并行加载多个资源
async function loadResources() {
try {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
} catch (error) {
console.error('加载资源失败:', error);
}
}
Promise.race的超时控制
下面仅让返回链超时拒绝,不会中止 fetch,也不会自动清理定时器。真实请求应结合 AbortSignal 并在结束时清理资源,见 取消、超时与错误边界。
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
最佳实践
- 始终处理错误:使用catch捕获Promise链中的错误
- 避免Promise嵌套:使用链式调用代替嵌套
- 合理使用Promise.all:并行执行独立的异步操作,提高效率
- 注意错误处理:Promise.all中一个失败会导致全部失败,如需单独处理可使用Promise.allSettled
// 使用Promise.allSettled获取所有结果,无论成功失败
Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});
面试要点
- 能够清晰解释Promise的三种状态及其转换(深入阅读:Promise 的三种状态)
- 理解Promise如何解决回调地狱问题(深入阅读:Promise 的链式调用)
- 掌握Promise.all和Promise.race的区别和使用场景(深入阅读:两者的区别)
- 了解async/await与Promise的关系(async函数返回Promise)(深入阅读:async 函数)
- 能够手写简单的Promise实现(深入阅读:手写 Promise 的最小实现范围)
来源与取舍
结合 JS异步编程.pdf 第 3–7 页补充单次决议、thenable、reaction 入队与链式传播。修正“微任务只由 resolve/reject 创建”“采用 thenable 的 return 值”和 finally 与 then 混用的表述;基础 API 示例继续在本篇和 ES6 专题维护。