先记住这个答案
Promise 的设计保证状态只能从 pending 转换到 fulfilled 或 rejected 一次。一旦状态确定,再次调用 resolve 或 reject 不会产生任何效果。例如,先 resolve 后 reject,Promise 会保持 fulfilled,reject 被忽略;反之亦然。这是为了避免竞态条件和状态不一致,确保 then 回调只触发一次。
- 首次状态转换生效,后续调用忽略
- then 回调仅执行一次,无竞态
- resolve 传入另一个 Promise 会递归等待
状态转换的严格性与内部实现
Promise 内部维护一个状态变量,初始为 pending。当调用 resolve 或 reject 时,引擎会先检查当前状态,只有 pending 才允许转变。一旦转变为 fulfilled 或 rejected,状态便永久固定。这一设计基于 ECMAScript 规范中的 PromiseResolveThenableJob 和 PromiseReject 操作,确保状态转换的原子性。
多次调用 resolve 或 reject 不会报错,也不会覆盖已有状态。例如先 resolve 后 reject,Promise 保持 fulfilled,拒绝原因被丢弃;若先 reject 再 resolve,则保持 rejected。这种忽略体现了“状态不可变”的核心原则,保证后续 then 回调只会收到一致的结果。
const p = new Promise((resolve, reject) => {
resolve('first');
reject(new Error('ignored'));
resolve('second');
});
p.then(
value => console.log('fulfilled:', value),
reason => console.log('rejected:', reason.message)
);
// 由于 resolve 先被调用,Promise 保持 fulfilled,后续 reject 和 resolve 被忽略
// 输出:fulfilled: first查看输出与解释
fulfilled: firstexecutor 同步执行,第一个 resolve 将状态设为 fulfilled,后续调用均无效,then 只注册一次。
缓存异步操作结果:防止重复初始化
在单页应用中常需缓存一个异步请求的结果,避免多次点击触发重复网络请求。假设用一个 promise 缓存用户数据,请求完成后无论后续调用多少次初始化函数,都应只发出一次请求并复用缓存。
实现中,初始化函数内创建 promise,并在 resolve 后挂到全局变量。由于状态不可变,后续调用同一个 promise 时,then 回调始终获得首次结果,不会重置。若没有该机制,多次调用可能导致多次请求或状态混乱。
let cachedPromise = null;
function getUserData() {
if (!cachedPromise) {
cachedPromise = new Promise((resolve) => {
// 模拟请求
setTimeout(() => resolve({ id: 1, name: 'Alice' }), 100);
});
}
return cachedPromise;
}
// 多次调用返回同一 promise,仅首次请求
const p1 = getUserData();
const p2 = getUserData();
console.log(p1 === p2); // true
p1.then(data => console.log('p1', data.name));
p2.then(data => console.log('p2', data.name));
// 输出:true 后两个日志顺序可能不同,但数据一致查看输出与解释
true
p1 Alice
p2 Alice缓存 promise 引用,状态锁定保证二次调用不再发起新请求,所有 then 获得相同结果。
状态锁定的边界与特殊情况
如果 resolve 的参数是另一个 promise(thenable),原始 promise 会立即被锁定(resolved)到该内部 promise,但其状态(settled state)会保持 pending 直到内部 promise 落定。此后,外层 promise 的状态由内部 promise 决定,且任何后续调用 resolve/reject 都无效。例如 resolve(promiseA) 后,若 promiseA 尚未完成,外层 promise 仍处于 pending,但后续调用 resolve/reject 会被忽略。
另一种边界是 executor 中抛出异常。同步抛出会触发 reject,等价于调用 reject(error),并同样遵循一次状态转换原则。若后面再调用 resolve 则无效。了解这些边界有助于避免在复杂异步流中误以为可以重置 promise 状态,实际需重新创建新 promise。
容易答错的地方
- 认为多次调用会累积或覆盖
- 错误认为多次 resolve/reject 可以改变最终结果。实际只有第一次有效,后续调用被完全忽略。例如先 resolve(1) 再 resolve(2),最终值仍是 1,不会出现覆盖。
- 以为会抛出异常或错误
- 有人担心多次调用会导致异常或未处理错误。实际上,后续调用被静默忽略,不产生任何错误提示。但也因此可能掩盖逻辑错误,比如在条件分支中误调 resolve 后未能发现。
面试官还会怎么问?
resolve 传入一个 Promise 后,再调用 reject 会怎样?
外层 promise 先锁定为 resolved to the inner promise,即内部 promise 落定后,外层状态将跟随内部,后续 reject 被忽略。例如 resolve(inner) 后调用 reject,最终取决于 inner 的完成状态,reject 无效。
如何判断一个 Promise 是否已经处于锁定状态?
JavaScript 没有公开 API 可以同步查询 promise 状态。可以通过注册 then 回调来观察,但回调在状态落定后才会异步执行,因此不能立即获知状态。也没有可靠的间接探测方法——例如,使用 Promise.race 并不适用,因为它无法区分 promise 是否已落定。
多次调用 resolve 但参数是 thenable,处理有何不同?
如果第一个 resolve 参数是 thenable,promise 会立即被锁定到该 thenable,并异步等待其落定;在此期间,后续调用 resolve/reject 都会被忽略,不会影响内部 promise 的落定。一旦内部 promise 落定,外层 promise 的最终状态随之确定,后续调用依然无效。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。