跳到主要内容

Promise 使用与原理:为什么能链式调用?回调为什么总比同步代码晚?

面试速答(30 秒版 TL;DR)

  • Promise 是对“未来结果”的统一抽象,状态只有 pendingfulfilledrejected 三种,而且一旦落定就不可逆。
  • thencatchfinally 都会返回一个 新的 Promise,所以 Promise 的链式调用本质是“前一个回调的执行结果决定后一个 Promise 的状态”。
  • Promise 回调不会同步立即执行,而是进入 微任务(microtask) 队列,所以总在当前同步代码之后执行。
  • 高频静态方法要会:resolverejectallallSettledraceany;同时要能说清 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 -> fulfilled
  • pending -> rejected

不会出现:

  • fulfilled -> rejected
  • rejected -> 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"

这段链路的底层规则是:

  1. 每次 then 都新建一个 Promise
  2. 如果回调返回普通值,后一个 Promise 以这个值 fulfilled
  3. 如果回调抛异常,后一个 Promise 以该异常 rejected
  4. 如果回调返回 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 函数返回 Promise
  • await 等待 Promise 落定
  • await 后面的逻辑会放到微任务里继续执行
  • 所以 async/await 不是替代 Promise,而是 Promise 的语法糖
async function load() {
const user = await fetchUser();
return user.name;
}

九、原理:手写 Promise 至少要实现什么

你不需要背规范全文,但要能说出 4 个核心点:

  1. Promise 需要维护状态和值/原因
  2. 状态只能从 pending 落定一次
  3. then 要能收集回调,并异步触发
  4. 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.allallSettled 有什么区别?

  • 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 三态:pendingfulfilledrejected
  • 回调走微任务,所以总晚于同步代码。
  • then 返回新 Promise,所以能链式调用。
  • all 看全成功,allSettled 看全结果,race 看谁先落定,any 看谁先成功。