Promise 使用与原理:为什么能链式调用?回调为什么总比同步代码晚?
面试速答(30 秒版 TL;DR)
- Promise 是对“未来结果”的统一抽象,状态只有
pending、fulfilled、rejected三种,而且一旦落定就不可逆。 then、catch、finally都会返回一个 新的 Promise,所以 Promise 的链式调用本质是“前一个回调的执行结果决定后一个 Promise 的状态”。- Promise 回调不会同步立即执行,而是进入 微任务(microtask) 队列,所以总在当前同步代码之后执行。
- 高频静态方法要会:
resolve、reject、all、allSettled、race、any;同时要能说清async/await只是 Promise 的语法糖。
心智模型:Promise 不是“把异步变同步”,而是“把异步结果对象化”
如果没有 Promise,异步结果通常只能靠回调拿:
ajax("/user", (err, data) => {
if (err) return handleError(err);
render(data);
});
问题是:
- 成功和失败分散在回调里
- 多个异步组合困难
- 错误传播不统一
Promise 做的事是把“未来的成功值/失败原因”封装成一个对象,于是你可以统一地:
- 注册成功回调
- 注册失败回调
- 做链式变换
- 组合多个异步任务
一、三种状态与不可逆性
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve("ok"), 1000);
});
Promise 只会沿这两条路径流转:
pending -> fulfilledpending -> rejected
不会出现:
fulfilled -> rejectedrejected -> fulfilled
这就是“一旦 settled 就不再变化”。
二、最常用实例方法
1) then(onFulfilled?, onRejected?)
fetchUser()
.then((user) => user.id)
.then((id) => fetchPosts(id))
.then((posts) => console.log(posts));
关键点:
then总是返回一个新的 Promise- 回调里
return 普通值,下一个 Promise 变 fulfilled - 回调里
return Promise,下一个 Promise 会“接管”它的状态 - 回调里
throw,下一个 Promise 变 rejected
2) catch(onRejected)
fetchUser()
.then((user) => riskyTransform(user))
.catch((err) => {
console.error(err);
return null;
});
本质上:
p.catch(fn);
// 约等于
p.then(undefined, fn);
但实践中更推荐把 catch 放在链尾统一兜底,可读性更好。
3) finally(onFinally)
loading = true;
fetchUser()
.finally(() => {
loading = false;
});
要点:
finally适合收尾,不适合做业务值变换- 它默认不会吞掉原结果
- 但如果
finally自己抛错或返回 rejected Promise,整个链会变失败
三、静态方法怎么记
| 方法 | 作用 | 常见场景 |
|---|---|---|
Promise.resolve(x) | 构造成功 Promise | 统一返回值 |
Promise.reject(err) | 构造失败 Promise | 快速失败 |
Promise.all(iterable) | 全成功才成功 | 并发请求,缺一不可 |
Promise.allSettled(iterable) | 全部结束再返回 | 批量任务汇总 |
Promise.race(iterable) | 谁先落定用谁 | 超时控制、竞速 |
Promise.any(iterable) | 谁先成功用谁 | 多路兜底 |
Promise.all
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
特点:
- 并发执行
- 任意一个失败,整体立即失败
- 返回结果顺序按输入顺序,不按完成顺序
Promise.allSettled
const results = await Promise.allSettled([taskA(), taskB()]);
典型返回值:
[
{ status: "fulfilled", value: "A" },
{ status: "rejected", reason: new Error("B") },
]
Promise.race
await Promise.race([
fetch("/api"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), 3000)
),
]);
注意:
- 比的是“谁先 settled”
- 先成功就成功,先失败就失败
Promise.any
await Promise.any([cdnA(), cdnB(), cdnC()]);
特点:
- 第一个成功就返回
- 全部失败才 reject
- 全部失败时通常抛
AggregateError
四、为什么 then 总在同步代码之后执行
Promise.resolve(1).then((v) => console.log("then", v));
console.log("sync");
// sync
// then 1
原因:
then/catch/finally的回调不会立刻同步执行- 它们会被放入 微任务队列
- 当前同步代码执行完后,事件循环会清空微任务
这也是 Promise 和普通同步函数调用最大的执行时序区别。
五、链式调用为什么成立
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => Promise.resolve(x + 1))
.then((x) => {
throw new Error(String(x));
})
.catch((e) => console.log(e.message)); // "3"
这段链路的底层规则是:
- 每次
then都新建一个 Promise - 如果回调返回普通值,后一个 Promise 以这个值 fulfilled
- 如果回调抛异常,后一个 Promise 以该异常 rejected
- 如果回调返回 Promise/thenable,后一个 Promise 会等待它并“吸收”其状态
这套规则通常叫 Promise Resolution Procedure。
六、executor 是同步执行的
这是高频坑。
new Promise(() => {
console.log("executor");
});
console.log("after");
// executor
// after
结论:
new Promise(executor)里的executor会同步立即执行- 异步的是后续状态通知回调,不是 executor 本身
七、错误传播为什么能“冒泡”
Promise.resolve()
.then(() => {
throw new Error("x");
})
.then(() => console.log("never"))
.catch((err) => console.log(err.message)); // "x"
因为:
then里的异常会把“返回的新 Promise”变成 rejected- 后续链条如果没有处理中间错误,会一直向后传播到最近的
catch
这和同步代码里的异常冒泡类似,但承载容器换成了 Promise 链。
八、和 async/await 的关系
面试标准答法:
async函数返回 Promiseawait等待 Promise 落定await后面的逻辑会放到微任务里继续执行- 所以
async/await不是替代 Promise,而是 Promise 的语法糖
async function load() {
const user = await fetchUser();
return user.name;
}
九、原理:手写 Promise 至少要实现什么
你不需要背规范全文,但要能说出 4 个核心点:
- Promise 需要维护状态和值/原因
- 状态只能从
pending落定一次 then要能收集回调,并异步触发then返回新 Promise,并能处理普通值、异常、Promise/thenable
极简示意:
class MyPromise {
constructor(executor) {
this.state = "pending";
this.value = undefined;
this.reason = undefined;
this.fulfilledQueue = [];
this.rejectedQueue = [];
const resolve = (value) => {
if (this.state !== "pending") return;
this.state = "fulfilled";
this.value = value;
queueMicrotask(() => {
this.fulfilledQueue.forEach((fn) => fn(value));
});
};
const reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
this.reason = reason;
queueMicrotask(() => {
this.rejectedQueue.forEach((fn) => fn(reason));
});
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
}
真实实现还要补:
- thenable 吸收
- 链式返回
- 循环引用检测
- 多次
resolve/reject保护
典型题 & 标准答法
Q1:Promise 为什么能链式调用?
因为 then/catch/finally 都返回新的 Promise,而新 Promise 的状态由前一个回调执行结果决定。
Q2:Promise.all 和 allSettled 有什么区别?
all:任何一个失败就整体失败allSettled:不短路,等所有任务完成后统一拿结果
Q3:为什么 then 总在同步代码之后?
因为 Promise 回调会进入微任务队列,当前调用栈清空后再执行。
Q4:Promise.resolve(Promise.resolve(1)) 会怎样?
会吸收内部 Promise,最终得到 fulfilled,值是 1。
易错点/坑
- 以为
new Promise里的 executor 是异步执行的,错,它是同步执行。 - 以为
then会修改原 Promise,错,它返回的是新 Promise。 - 在
Promise.all之前先一个个await,会把并发写成串行。 finally适合清理,不适合承载业务返回值变换。
速记要点(可背诵)
- Promise 三态:
pending、fulfilled、rejected。 - 回调走微任务,所以总晚于同步代码。
then返回新 Promise,所以能链式调用。all看全成功,allSettled看全结果,race看谁先落定,any看谁先成功。