Promise 深入
Promise 是解决异步编程的核心工具,它把"回调嵌套"改写成"链式调用",让异步代码更易读、易维护。本文从状态机讲起,覆盖链式调用、错误处理、全部静态方法,再到取消机制与常见面试题,帮助你彻底掌握 Promise。
一、Promise 概念与三种状态
Promise(期约) 表示一个异步操作的最终结果。它始终处于以下三种状态之一:
| 状态 | 英文 | 含义 | 是否可变化 |
|---|---|---|---|
| 待定 | pending | 异步操作进行中 | 可变为 fulfilled/rejected |
| 已兑现 | fulfilled | 操作成功,有结果值 | 不可逆 |
| 已拒绝 | rejected | 操作失败,有失败原因 | 不可逆 |
const p = new Promise((resolve, reject) => {
// 异步操作成功时调用 resolve(值),失败时调用 reject(原因)
});
console.log(p); // Promise { <pending> }1.1 状态不可逆转换
状态只能从 pending 变为 fulfilled 或 rejected,一旦改变就永久定型,再次调用 resolve/reject 无效:
const p = new Promise((resolve, reject) => {
resolve("成功");
reject("失败"); // 无效,状态已定型
resolve("再次成功"); // 无效
});
p.then((value) => console.log("结果:", value)); // 结果:成功1.2 executor 同步执行
new Promise 传入的函数称为 executor,它立即同步执行,只是其中的异步操作结果要等后续回调处理:
console.log("1");
new Promise((resolve) => {
console.log("2");
resolve();
}).then(() => console.log("4"));
console.log("3");
// 输出:1 2 3 4(executor 同步,then 回调是微任务)二、then 链式调用与返回值透传
2.1 then 返回新 Promise
then 返回一个新的 Promise,因此可以无限链式调用。回调的返回值决定下一个 Promise 的状态:
Promise.resolve("a")
.then((v) => v + "b") // 返回普通值 → 新 Promise 兑现为 "ab"
.then((v) => v + "c") // "abc"
.then((v) => console.log(v)); // abc2.2 返回值类型规则
| 回调返回值 | 下一个 Promise 的结果 |
|---|---|
| 普通值 | 兑现为该值 |
没有返回值(undefined) | 兑现为 undefined |
| 一个新的 Promise | 状态与"跟随"该 Promise |
| 抛出异常 | 拒绝为该异常 |
Promise.resolve(1)
.then((v) => Promise.resolve(v * 10)) // 返回 Promise,跟随其状态
.then((v) => {
throw new Error("故意出错");
})
.then(() => console.log("不会执行"))
.catch((e) => console.log(e.message)); // 故意出错2.3 不返回就"透传"
链中某个 then 没有返回值时,undefined 会继续向后传递,但常见场景是透传前面的值:
Promise.resolve("数据")
.then((data) => {
console.log("处理中:", data);
// 没有 return → 下一环收到 undefined
})
.then((v) => console.log("收到:", v)); // 收到: undefined三、catch 与错误传播
3.1 catch 捕获链中任意一环的错误
错误会沿着链向后传播,直到遇到 catch:
Promise.resolve(1)
.then((v) => {
throw new Error("第 1 环出错");
})
.then(() => console.log("不会执行"))
.then(() => console.log("也不会执行"))
.catch((e) => console.log("捕获到:", e.message)); // 捕获到:第 1 环出错3.2 Promise 内错误自动捕获
executor 中抛出的异常会被自动转为拒绝,无需手动 try/catch:
const p = new Promise(() => {
throw new Error("executor 中抛出");
});
p.catch((e) => console.log(e.message)); // executor 中抛出同样,then/catch 回调中抛出的异常也会自动让返回的 Promise 拒绝:
Promise.resolve()
.then(() => {
JSON.parse("{非法 json"); // 抛出异常
})
.catch((e) => console.log("解析失败:", e.message));3.3 catch 之后可以继续
catch 本身也会返回 Promise,捕获错误后链可以继续:
Promise.reject("错误")
.catch((e) => {
console.log("记录错误:", e);
return "恢复值"; // 之后链恢复正常
})
.then((v) => console.log("继续:", v)); // 继续:恢复值3.4 未捕获的拒绝
链尾如果没有 catch,拒绝会成为 unhandledrejection(浏览器事件)或 unhandledRejection(Node 事件),导致进程报错。任何 Promise 链都要以 catch 结尾。
Promise.reject("孤儿拒绝");
// 浏览器控制台:Uncaught (in promise) 孤儿拒绝四、finally
finally 无论成功失败都会执行,适合做清理工作(关闭加载动画、释放资源)。它不接收结果值,也不会改变链的状态:
let loading = true;
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve("数据"), 100);
});
}
fetchData()
.then((data) => console.log(data))
.finally(() => {
loading = false; // 无论成败都关闭 loading
console.log("清理完成");
});注意:finally 返回的 Promise 会透传原状态,除非 finally 回调本身抛出异常(此时变为拒绝):
Promise.resolve("成功")
.finally(() => console.log("finally"))
.then((v) => console.log("透传:", v));
// finally → 透传:成功
Promise.reject("失败")
.finally(() => {})
.catch((e) => console.log("仍然拒绝:", e)); // 仍然拒绝:失败五、静态方法逐一详解
5.1 Promise.resolve 与 Promise.reject
// 包装一个值成已兑现的 Promise
const p1 = Promise.resolve(42);
p1.then((v) => console.log(v)); // 42
// 传 Promise 时直接复用
const original = new Promise((r) => r(1));
console.log(Promise.resolve(original) === original); // true
// 包装成已拒绝的 Promise
const p2 = Promise.reject(new Error("原因"));
p2.catch((e) => console.log(e.message)); // 原因5.2 Promise.all —— 全部成功才成功
所有 Promise 都兑现才兑现(结果为数组),任一拒绝则立即整体拒绝:
const p1 = Promise.resolve(1);
const p2 = new Promise((r) => setTimeout(() => r(2), 100));
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((results) => console.log(results)); // [1, 2, 3]
// 失败场景
Promise.all([Promise.resolve(1), Promise.reject("出错")])
.then((r) => console.log("不会执行"))
.catch((e) => console.log("整体拒绝:", e)); // 整体拒绝:出错5.3 Promise.allSettled —— 等待全部完成
不关心成败,等待所有 Promise 完成,返回每个结果的状态描述:
Promise.allSettled([Promise.resolve(1), Promise.reject("失败")]).then(
(results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: '失败' }
// ]
}
);5.4 Promise.race —— 谁先到用谁
返回最先定型(成功或失败)的 Promise 的结果,常用于超时控制:
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error("请求超时")), ms)
);
}
function request() {
return new Promise((resolve) => setTimeout(() => resolve("响应数据"), 200));
}
Promise.race([request(), timeout(100)])
.then((v) => console.log(v))
.catch((e) => console.log(e.message)); // 请求超时(先到达的 timeout 胜出)5.5 Promise.any —— 第一个成功
只要有一个兑现即返回该结果;全部拒绝才拒绝(错误为 AggregateError,包含所有原因):
Promise.any([
Promise.reject("A 失败"),
Promise.resolve("B 成功"),
Promise.reject("C 失败"),
]).then((v) => console.log(v)); // B 成功
Promise.any([Promise.reject(1), Promise.reject(2)])
.then(() => {})
.catch((e) => {
console.log(e instanceof AggregateError); // true
console.log(e.errors); // [1, 2]
});5.6 静态方法对比
| 方法 | 成功条件 | 失败条件 | 结果形式 | 典型场景 |
|---|---|---|---|---|
all | 全部兑现 | 任一拒绝(立即) | 值数组 | 并发请求后汇总 |
allSettled | 全部完成 | 永不整体失败 | 状态对象数组 | 不关心成败的批量任务 |
race | 任一先兑现 | 任一先拒绝 | 最先定型的值 | 超时控制 |
any | 任一兑现 | 全部拒绝 | 第一个兑现的值 | 多路冗余请求 |
5.7 Promise.withResolvers
ES2024 新增,一次获得 resolve/reject 与 Promise 本体,适合在非 executor 环境中控制 Promise:
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve("外部触发的成功"), 100);
promise.then((v) => console.log(v)); // 外部触发的成功传统写法需要在外层持有变量,withResolvers 更简洁。
六、Promise 链与回调地狱对比
6.1 回调地狱
// 嵌套回调:逻辑越深越难读
getUser(1, (user) => {
getOrders(user.id, (orders) => {
getDetail(orders[0].id, (detail) => {
console.log(detail);
});
});
});6.2 Promise 链
getUser(1)
.then((user) => getOrders(user.id))
.then((orders) => getDetail(orders[0].id))
.then((detail) => console.log(detail))
.catch((e) => console.error(e));| 对比项 | 回调地狱 | Promise 链 |
|---|---|---|
| 可读性 | 嵌套缩进,随层级加深 | 扁平顺序,一目了然 |
| 错误处理 | 每层需单独处理 | 链尾一个 catch 统一处理 |
| 顺序控制 | 手工维护 | 天然按返回顺序传递 |
七、错误处理模式
7.1 链尾统一 catch
fetchData()
.then(parseData)
.then(renderUI)
.catch((e) => showError(e.message)); // 集中处理所有环节的错误7.2 async/await 中 try/catch
await 让 Promise 链看起来像同步代码,用 try/catch 捕获:
async function load() {
try {
const user = await getUser(1);
const orders = await getOrders(user.id);
return orders;
} catch (e) {
console.error("加载失败:", e.message);
return [];
}
}7.3 局部降级
某一步失败不影响后续,可以在该步就地 catch 并返回默认值:
fetchConfig()
.catch(() => ({ theme: "default" })) // 配置加载失败用默认值
.then((config) => initUI(config));八、then 回调的异步性
then/catch/finally 的回调总是作为微任务异步执行,即使 Promise 已同步兑现:
console.log("同步开始");
Promise.resolve().then(() => console.log("then 回调"));
console.log("同步结束");
// 输出:同步开始 → 同步结束 → then 回调这保证回调永远不会在 executor 结束前执行,避免了"结果还没就绪"的竞态。
九、实现 Promise.all 的思路
手动实现 Promise.all 的核心思路:
- 遍历每个 Promise;
- 维护结果数组与完成计数;
- 全部成功 → 兑现结果数组;
- 任一失败 → 整体拒绝。
function myAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let done = 0;
promises.forEach((p, index) => {
Promise.resolve(p) // 兼容非 Promise 值
.then((value) => {
results[index] = value; // 按下标存放,保证顺序
done++;
if (done === promises.length) {
resolve(results);
}
})
.catch(reject); // 任一失败立即拒绝
});
});
}
// 使用
myAll([Promise.resolve(1), 2, Promise.resolve(3)]).then((r) =>
console.log(r)
); // [1, 2, 3]要点:用 Promise.resolve(p) 兼容普通值;结果按下标存放而不是 push,防止异步完成顺序打乱结果顺序;空数组时直接兑现空数组。
十、取消 Promise(AbortController)
Promise 本身不可取消,但可以通过 AbortController 配合底层异步 API 实现取消语义:
const controller = new AbortController();
const { signal } = controller;
// 模拟可取消的异步任务
function createCancelableTask(signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve("任务完成"), 3000);
signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new Error("任务已取消"));
});
});
}
createCancelableTask(signal)
.then((v) => console.log(v))
.catch((e) => console.log(e.message));
setTimeout(() => controller.abort(), 500);
// 500ms 后输出:任务已取消fetch 原生支持 signal 参数:
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal }).catch((e) => {
if (e.name === "AbortError") console.log("请求被取消");
});十一、Promise 面试题
11.1 经典执行顺序
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => {
console.log("promise1");
Promise.resolve().then(() => console.log("promise2"));
})
.then(() => console.log("promise3"));
console.log("end");
// 输出:start → end → promise1 → promise2 → promise3 → timeout分析:promise1 所在 then 返回的是内部产生的 Promise,promise3 的 then 排在它之后入微任务队列,因此 promise2 先于 promise3;全部微任务清空后才执行宏任务 timeout。
11.2 await 与 then 的等价
async function test() {
const a = await 1; // await 普通值 → 兑现为 1
const b = await Promise.resolve(2);
console.log(a + b); // 3
}
test();11.3 返回自身会怎样
then 回调返回它所在链产生的那个 Promise 会报错(避免死循环),实践中不要返回链上自身:
const p = Promise.resolve();
const q = p.then(() => q); // TypeError: Chaining cycle detected十二、实战建议
- 每个链都以 catch 结尾,避免 unhandled rejection 静默吞错;
- 并发独立任务用
Promise.all,失败可接受用allSettled,需要"最先完成"用race/any; - 需要取消或超时控制的请求统一使用
AbortController; - 复杂异步流程优先考虑
async/await,配合try/catch可读性最佳; - 不要在
then回调中做重计算,保持回调小而专注。