一、Promise 面试知识总图
text
Promise
│
├── 1. 基础概念
│ ├── Promise 是什么
│ ├── 三种状态
│ ├── resolve / reject
│ ├── executor 是否异步
│ └── Promise 状态是否可逆
│
├── 2. then 链
│ ├── then 返回什么
│ ├── 为什么可以链式调用
│ ├── return 普通值
│ ├── return Promise
│ ├── throw Error
│ └── then / catch 错误传播
│
├── 3. Promise Resolution Procedure
│ ├── resolve(Promise)
│ ├── resolve(thenable)
│ ├── Promise.resolve()
│ └── Promise 的"状态跟随"
│
├── 4. 异步调度
│ ├── 微任务
│ ├── then 为什么异步
│ ├── Promise + setTimeout
│ └── async / await 与 Promise
│
├── 5. 静态方法
│ ├── Promise.all
│ ├── Promise.race
│ ├── Promise.allSettled
│ └── Promise.any
│
├── 6. 错误处理
│ ├── catch
│ ├── finally
│ ├── 未处理拒绝
│ └── 错误吞掉问题
│
├── 7. 手写 Promise
│ ├── Promise 构造器
│ ├── then
│ ├── 链式调用
│ ├── 异步回调
│ ├── thenable
│ └── 循环引用
│
└── 8. 工程问题
├── 并发控制
├── 请求超时
├── 请求取消
├── 重试
├── 请求竞态
└── Promise 是否等于并行
二、第一层:Promise 基础高频题
1. Promise 是什么?
核心思路(一句话)
Promise 本质上就是一个表示异步操作最终结果的对象。这个结果要么成功,要么失败;我们可以通过 then、catch 等方式处理这个结果。
满分答案
它解决的核心问题不是"让 JavaScript 变成异步",而是:
把异步操作的成功结果、失败原因以及后续处理统一抽象成一个对象。
Promise 有三种状态:
text
pending
│
├── resolve(value) ──→ fulfilled
│
└── reject(reason) ──→ rejected
状态一旦从 pending 变成 fulfilled 或 rejected,就不能再次改变。
Promise 本身并不会让任务自动变成异步;例如:
javascript
new Promise(() => {
console.log('hello');
});
这里的 console.log 会同步执行。
真正异步的是:
javascript
Promise.resolve().then(() => {
console.log('hello');
});
因为 then 回调会进入微任务队列。(MDN Web Docs)
需要补一句
Promise 的核心价值是"异步结果的统一表示和组合",而不是单纯解决回调地狱。
这是比"Promise 是异步编程解决方案"更准确的回答。
三、Promise 有哪些状态?状态为什么不可逆?
2. Promise 状态有哪些?
满分答案
三个:
text
pending 等待中
fulfilled 已成功
rejected 已失败
状态转换只有:
text
resolve
pending ─────────────→ fulfilled
│
│ reject
↓
rejected
不能:
text
fulfilled → rejected
rejected → fulfilled
例如:
javascript
const p = new Promise((resolve, reject) => {
resolve(1);
reject(2);
});
p.then(console.log);
结果:
text
1
因为 Promise 一旦完成,后续的 resolve/reject 不再改变它的最终状态。
关键追问
"那 resolve() 为什么还可以传 Promise?"
这就进入 Promise 最重要的高级知识:
resolve 不一定意味着"立即 fulfilled",它可能意味着"采用另一个 Promise 的最终状态"。
这是很多普通答案没有讲清楚的地方。
四、Promise 中最容易被问的:resolve 到底做了什么?
3. resolve(promise) 是立即成功吗?
核心思路
不一定。resolve(x) 的真正含义不是"把 Promise 成功值设置成 x",而是让当前 Promise 的状态跟随 x。
例如:
javascript
const p1 = new Promise(resolve => {
resolve(
new Promise(resolve2 => {
setTimeout(() => resolve2(1000), 1000);
})
);
});
p1.then(console.log);
最终:
text
1秒后
1000
不是:
text
Promise { ... }
原理
可以理解成:
text
resolve(x)
│
├── x 是普通值
│ ↓
│ fulfilled(x)
│
├── x 是 Promise
│ ↓
│ 跟随它的状态
│
└── x 是 thenable
↓
调用它的 then
↓
跟随它最终状态
这就是 Promise 的 Promise Resolution Procedure(承诺解析过程)。
Promise.resolve() 同样会处理 thenable,并跟随其最终状态。(MDN Web Docs)
五、Promise.resolve() 为什么是高频追问?
4. Promise.resolve() 和 new Promise() 有什么区别?
满分答案
最核心区别:
javascript
Promise.resolve(value)
主要用于:
把一个值统一转换成 Promise。
例如:
javascript
Promise.resolve(100)
相当于得到:
text
fulfilled(100)
而:
javascript
new Promise((resolve, reject) => {
// 自己控制什么时候成功或失败
});
主要用于:
创建一个
由自己控制完成时机的 Promise。
例如:
javascript
const p = new Promise(resolve => {
setTimeout(() => resolve(100), 1000);
});
一个非常重要的细节
javascript
const p = Promise.resolve(1);
console.log(p);
这里得到的是一个 Promise。而:
javascript
Promise.resolve(p) === p
在原生 Promise 场景下可以为 true。但如果传入的是其他 Promise 实现或者 thenable,就需要经过解析过程。
六、then 是 Promise 面试的核心
5. then() 返回什么?
核心思路
then() 一定返回一个新的 Promise,这个新 Promise 的状态和值由回调函数的执行结果决定。
例如:
javascript
const p1 = Promise.resolve(1);
const p2 = p1.then(value => {
return value + 1;
});
console.log(p2);
结果:
text
p2 → fulfilled(2)
所以:
text
p1
│
│ then
↓
p2
│
│ then
↓
p3
这就是 Promise 链。
七、then 返回普通值、Promise、抛异常分别是什么结果?
6. then() 回调返回不同东西会怎么样?
这是必须掌握的核心题。
情况一:返回普通值
javascript
Promise.resolve(1)
.then(value => {
return value + 1;
})
.then(console.log);
结果:
text
2
本质:
text
return 2
↓
新 Promise fulfilled(2)
情况二:返回 Promise
javascript
Promise.resolve(1)
.then(() => {
return Promise.resolve(2);
})
.then(console.log);
结果:
text
2
不是:
text
Promise { 2 }
因为新 Promise 会采用返回 Promise 的最终状态。
情况三:抛异常
javascript
Promise.resolve(1)
.then(() => {
throw new Error('error');
})
.catch(console.error);
结果:
text
rejected
可以把它记成:
text
then 回调:
return 普通值
↓
新 Promise fulfilled
return Promise
↓
新 Promise 跟随它
throw Error
↓
新 Promise rejected
这张图是 Promise 链最重要的知识。
八、为什么 Promise 可以链式调用?
7. Promise 链式调用的底层原因是什么?
满分答案
不是因为 Promise 有什么特殊语法,而是因为:
then()每次都会返回一个新的 Promise。
所以:
javascript
p
.then(fn1)
.then(fn2)
.then(fn3);
实际上是:
text
p
↓
then(fn1)
↓
p2
↓
then(fn2)
↓
p3
↓
then(fn3)
↓
p4
每个 then 都在处理上一个 Promise 的结果。所以 Promise 链实际上是一条:
"异步结果传递链"。
九、then(success, error) 和 catch() 有什么区别?
8. then(null, error) 和 catch(error) 一样吗?
满分答案
对于错误处理来说:
javascript
promise.catch(errorHandler)
本质上等价于:
javascript
promise.then(undefined, errorHandler)
但面试时必须注意一个非常重要的区别:
javascript
promise
.then(
value => {
throw new Error('error');
},
error => {
// 这里捕获不到上面 success 回调抛出的错误
}
);
因为第二个参数处理的是:
当前 Promise 的 rejection
而不是:
第一个 success 回调执行过程中产生的异常。
所以更常见的写法:
javascript
promise
.then(value => {
throw new Error('error');
})
.catch(error => {
console.log(error);
});
一句话记忆
text
then(success, error)
↑
只能处理前一个 Promise
then(success)
↓
如果 success 自己出错
↓
返回的新 Promise rejected
↓
后面的 catch 才能捕获
十、finally() 到底怎么工作?
9. finally() 有什么特点?
核心思路
finally() 不关心成功还是失败,它主要用于执行无论如何都要执行的清理逻辑。
例如:
javascript
fetch('/api')
.then(...)
.catch(...)
.finally(() => {
loading = false;
});
最大的面试点
javascript
Promise.resolve(1)
.finally(() => {
return 2;
})
.then(console.log);
输出:
text
1
因为 finally 正常返回一个普通值时,不会改变原来的结果。
但:
javascript
Promise.resolve(1)
.finally(() => {
throw new Error('finally error');
})
.catch(console.log);
会变成 rejected。
所以:
text
finally 正常结束
↓
保持原来的成功/失败结果
finally 抛异常
↓
覆盖原结果 → rejected
finally 返回 rejected Promise
↓
覆盖原结果 → rejected
十一、Promise + Event Loop
10. 为什么 Promise.then() 是异步的?
javascript
console.log(1);
Promise.resolve().then(() => {
console.log(2);
});
console.log(3);
结果:
text
1
3
2
原因
then 回调不会立即执行,而是进入微任务队列。
text
同步代码
↓
调用 then
↓
注册回调
↓
当前同步代码执行完
↓
执行微任务
↓
then 回调执行
所以:
javascript
Promise.resolve().then(fn);
即使 Promise 已经 fulfilled:
text
fn 也不会同步执行
这是 Promise 保证异步回调一致性的关键行为。(MDN Web Docs)
十二、经典执行顺序题
11. 下面代码输出什么?
javascript
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve().then(() => {
console.log(3);
});
console.log(4);
答案:
text
1
4
3
2
推导
text
同步任务:
1
4
↓
微任务:
3
↓
下一轮任务:
2
注意点
不要简单回答:
"Promise 比 setTimeout 快。"
更准确的是:
Promise 的
then回调属于微任务,而setTimeout回调属于任务队列中的定时器任务;当前同步代码执行完成后,会先处理微任务,再进入后续任务。
十三、async / await 本质是什么?
12. async/await 和 Promise 什么关系?
核心思路
async/await 不是另一套异步模型,它是建立在 Promise 之上的语法形式。
例如:
javascript
async function test() {
const result = await getData();
return result;
}
本质上可以理解为:
text
Promise
↓
await 暂停当前 async 函数后续执行
↓
Promise fulfilled
↓
恢复后续执行
并且:
javascript
async function test() {
return 100;
}
返回的仍然是 Promise:
javascript
test().then(console.log);
输出:
text
100
而:
javascript
async function test() {
throw new Error('error');
}
返回的是 rejected Promise。
十四、await 到底是不是阻塞?
13. await 会阻塞 JavaScript 主线程吗?
标准答案:不会。
await 只是暂停:
当前 async 函数后面的代码。
不会阻塞整个 JavaScript 主线程。例如:
javascript
async function test() {
console.log(1);
await Promise.resolve();
console.log(2);
}
test();
console.log(3);
输出:
text
1
3
2
所以:
text
await
↓
暂停当前 async 函数
↓
把后续逻辑安排到 Promise 后续调度中
↓
主线程继续执行其他同步代码
十五、Promise.all 是最高频的静态方法题
14. Promise.all() 有什么特点?
核心思路
所有任务都成功才成功,只要一个失败就失败。
javascript
Promise.all([
getUser(),
getOrders(),
getProducts()
]);
适合:
多个任务互不依赖,并且必须全部成功才能继续。
例如:
text
页面初始化
├── 用户信息
├── 权限信息
└── 配置信息
全部成功
↓
页面渲染
最容易错的地方
javascript
Promise.all([p1, p2, p3])
如果 p2 失败:
text
Promise.all → rejected
但是:
p1、p3 并不会因此自动取消。
Promise.all 只是让聚合结果提前 rejected ,不是取消底层任务。(MDN Web Docs)
十六、Promise.all 的结果顺序是什么?
15. Promise.all 为什么结果顺序和完成顺序不一样?
javascript
const p1 = new Promise(resolve => {
setTimeout(() => resolve('A'), 3000);
});
const p2 = new Promise(resolve => {
setTimeout(() => resolve('B'), 1000);
});
Promise.all([p1, p2]).then(console.log);
结果:
javascript
['A', 'B']
不是:
javascript
['B', 'A']
因为:
Promise.all 按输入顺序组织结果,而不是按完成顺序。
所以:
text
p1 ─────── A
p2 ─ B
结果:
[A, B]
十七、Promise.all / race / allSettled / any 怎么区别?
16. 四个并发方法怎么选?
这是必须形成条件反射的一道题。
| 方法 | 成功条件 | 失败条件 | 典型场景 |
|---|---|---|---|
all |
全部成功 | 任意一个失败 | 必须全部成功 |
race |
第一个 settled | 第一个 settled 是失败 | 谁先结束用谁 |
allSettled |
全部结束 | 不会 rejected | 需要知道所有结果 |
any |
第一个成功 | 全部失败 | 谁先成功用谁 |
核心记忆:
text
all
→ 全部成功
race
→ 第一个结束
allSettled
→ 全部结束
any
→ 第一个成功
MDN 对四者的定义也是这一核心差异。(MDN Web Docs)
十八、Promise.race 和 Promise.any 最容易混淆
17. race() 和 any() 有什么区别?
race 看:
谁最先结束。
无论成功还是失败。
text
A reject ── 100ms
B fulfill ─ 200ms
race
↓
reject
any 看:
谁最先成功。
text
A reject ── 100ms
B fulfill ─ 200ms
any
↓
B
只有:
text
所有 Promise 都失败
时:
javascript
Promise.any(...)
才 rejected,并且错误是 AggregateError。(MDN Web Docs)
十九、Promise.race 能实现请求超时吗?
18. Promise.race() 如何实现请求超时?
经典答案:
javascript
function timeout(promise, ms) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, ms);
});
return Promise.race([
promise,
timeoutPromise
]);
}
使用:
javascript
timeout(fetch('/api'), 5000);
但是高级开发必须指出一个问题
这段代码只能实现:
"5 秒后返回超时结果"
并不意味着:
"5 秒后真正取消网络请求"。
因为 Promise.race() 不会自动取消输掉竞争的 Promise。(MDN Web Docs)
生产环境更合理的是:
javascript
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, 5000);
fetch('/api', {
signal: controller.signal
}).finally(() => {
clearTimeout(timer);
});
这是高级面试非常喜欢追问的点:
Promise 的"结果竞争"和"底层任务取消"是两个不同问题。
二十、Promise.allSettled 解决什么问题?
19. 为什么需要 Promise.allSettled()?
假设:
javascript
Promise.all([
upload1(),
upload2(),
upload3()
]);
其中一个失败。Promise.all() 会直接 rejected。但如果业务需求是:
三个文件全部上传完以后,我要知道每一个到底成功还是失败。
就应该:
javascript
const results = await Promise.allSettled([
upload1(),
upload2(),
upload3()
]);
结果类似:
javascript
[
{
status: 'fulfilled',
value: 'success'
},
{
status: 'rejected',
reason: new Error('失败')
},
{
status: 'fulfilled',
value: 'success'
}
]
所以:
text
all
→ 要求整体成功
allSettled
→ 要完整知道整体结果
二十一、Promise.any 的真实业务场景
20. Promise.any() 有什么实际用途?
非常典型:
多个 CDN / 服务节点同时请求,哪个先成功就使用哪个。
javascript
const result = await Promise.any([
fetch('https://cdn-a.com/data'),
fetch('https://cdn-b.com/data'),
fetch('https://cdn-c.com/data')
]);
特点:
text
A 失败
B 失败
C 成功
↓
使用 C
如果:
text
A 失败
B 失败
C 失败
才整体失败。
二十二、Promise 是否等于并发?
21. Promise 是并发还是并行?
这是非常重要的一题。
Promise 本身不等于并行。
JavaScript 主线程在同一时刻仍然只能执行一段 JavaScript。Promise 主要解决的是:
异步任务的组织、组合和结果传递。
例如:
javascript
const p1 = fetch('/a');
const p2 = fetch('/b');
const p3 = fetch('/c');
await Promise.all([p1, p2, p3]);
这里可以表现出:
text
请求 A ────────────────┐
请求 B ───────────┐ │
请求 C ───────────────┐│
↓↓↓
Promise.all
这属于异步并发。但不能因此说:
"三个 JavaScript 函数同时在 CPU 上执行。"
真正的 CPU 并行通常需要 Worker 等机制。(MDN Web Docs)
二十三、一个非常高频的坑:await 串行
22. 下面代码有什么性能问题?
javascript
const user = await getUser();
const orders = await getOrders();
const products = await getProducts();
如果三个请求互不依赖:
text
getUser
↓
完成
↓
getOrders
↓
完成
↓
getProducts
属于串行。
可以改成:
javascript
const [user, orders, products] = await Promise.all([
getUser(),
getOrders(),
getProducts()
]);
变成:
text
getUser ──────────┐
getOrders ────────┐ │
getProducts ────────┐│
↓↓↓
all
但不要机械使用 Promise.all
如果:
javascript
const user = await getUser();
const orders = await getOrders(user.id);
存在依赖关系,就不能简单并发。所以真正的判断标准是:
有没有数据依赖,而不是"看到 await 就改 Promise.all"。
二十四、Promise 手写题
23. 手写一个 Promise,面试官到底在考什么?
真正考的不是:
"你能不能写一个 Promise。"
而是:
text
状态管理
↓
异步回调保存
↓
then 链
↓
返回新 Promise
↓
结果传递
↓
异常传播
↓
thenable
↓
循环引用
所以面试如果让你手写 Promise,至少应该能逐层实现。
二十五、简化版 Promise
第一版:
javascript
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
const resolve = value => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
};
const reject = reason => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.reason = reason;
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
}
这只能说明:
text
Promise 状态只能改变一次
但还不够。
二十六、真正的 Promise 手写核心:then
需要增加:
javascript
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
// 根据当前 Promise 状态处理
});
}
如果当前是:
text
pending
就必须保存:
javascript
onFulfilled
onRejected
等未来状态改变以后再执行。所以 Promise 内部最核心的数据结构可以理解为:
text
Promise
│
├── state
├── value / reason
│
├── fulfilled callbacks
│
└── rejected callbacks
二十七、手写 Promise 时最关键的难点:then 返回新 Promise
例如:
javascript
const p2 = p1.then(value => {
return value + 1;
});
这里:
text
p1
↓
then
↓
p2
所以:
不能让 then 返回当前 Promise。
必须:
javascript
return new MyPromise(...)
然后根据:
javascript
onFulfilled(value)
的结果决定 p2:
text
普通值
↓
resolve(value)
Promise
↓
跟随 Promise
throw
↓
reject(error)
这就是 Promise 链的核心。
二十八、手写 Promise 最容易漏掉:thenable
例如:
javascript
const thenable = {
then(resolve) {
setTimeout(() => {
resolve(100);
}, 1000);
}
};
如果:
javascript
return thenable;
Promise 链也应该最终得到:
text
100
而不是把整个对象当成结果。因此 Promise 的解析逻辑实际上要判断:
text
x
│
├── 普通值
│
├── Promise
│
└── thenable
这也是 Promise 标准里非常重要的一部分。(MDN Web Docs)
二十九、Promise 循环引用
24. 为什么下面代码有问题?
javascript
let p2 = p1.then(() => {
return p2;
});
这里形成:
text
p2
↑
│
then 返回 p2
也就是:
text
Promise 自己等待自己
如果实现 Promise 时不检测这种情况,就可能永远 pending。所以一个完整 Promise 实现需要处理:
javascript
if (promise2 === x) {
return reject(
new TypeError('Chaining cycle')
);
}
这就是为什么真正的 Promise 手写题远比"写三个状态"复杂。
三十、Promise 错误传播
25. Promise 为什么可以"一直 catch 到最后"?
例如:
javascript
Promise.resolve()
.then(() => {
throw new Error('A');
})
.then(() => {
console.log('B');
})
.then(() => {
console.log('C');
})
.catch(error => {
console.log(error.message);
});
输出:
text
A
因为:
text
throw
↓
当前 then 返回 Promise rejected
↓
下一个 then 没有 rejection handler
↓
继续向后传递
↓
catch 捕获
所以可以把 Promise 的错误传播理解成:
text
rejected
↓
找下一个 rejection handler
↓
没有?
↓
继续向后传
↓
直到 catch
三十一、Promise 的错误为什么有时候"消失"?
26. 下面代码有什么问题?
javascript
Promise.reject(new Error('error'))
.catch(error => {
console.log(error);
});
这里错误实际上已经被处理了。
但如果:
javascript
Promise.reject(new Error('error'))
.catch(error => {
console.log(error);
return 'success';
});
后面的 Promise 会:
text
fulfilled('success')
也就是说:
catch 不只是"打印错误",它也是一个 Promise 链节点,可以把 rejected 转换成 fulfilled。
这是非常重要的。
三十二、Promise 的并发控制
27. 如果 1000 个请求只能同时运行 10 个,怎么实现?
应该设计:
text
任务池
│
├── Worker 1
├── Worker 2
├── ...
└── Worker 10
队列:
1 2 3 4 5 ... 1000
核心思想:
text
当前运行数 < 10
↓
取任务
↓
执行
↓
完成
↓
运行数 - 1
↓
继续取任务
一个可面试的实现
javascript
async function mapLimit(list, limit, worker) {
const results = new Array(list.length);
let nextIndex = 0;
async function run() {
while (true) {
const index = nextIndex++;
if (index >= list.length) {
return;
}
results[index] = await worker(
list[index],
index
);
}
}
const workers = Array.from(
{ length: Math.min(limit, list.length) },
run
);
await Promise.all(workers);
return results;
}
使用:
javascript
const result = await mapLimit(
urls,
10,
url => fetch(url)
);
这就体现了:
Promise 负责异步结果组织,业务代码负责控制并发度。
三十三、Promise 重试
28. 如何实现请求失败自动重试?
javascript
async function retry(fn, times) {
let lastError;
for (let i = 0; i < times; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
}
}
throw lastError;
}
使用:
javascript
const data = await retry(
() => fetch('/api'),
3
);
继续追问:
是不是所有请求都适合重试?
答案:
不是。
例如:
text
GET
查询
幂等操作
通常更适合重试。
而:
text
POST 创建订单
支付
扣库存
需要考虑:
text
幂等性
请求是否已经在服务端执行
重复提交风险
所以:
重试不是 Promise API 问题,而是分布式系统里的幂等性问题。
这个回答层次会明显高于单纯写代码。
三十四、Promise 请求竞态
29. 前端搜索框为什么会出现 Promise 竞态?
例如用户快速输入:
text
a
ab
abc
请求:
text
a ───────────────→ 结果 A
ab ───────→ 结果 B
abc ──→ 结果 C
如果:
text
C 先回来
A 最后回来
最终页面可能被:
text
A
覆盖。
这就是:
请求竞态。
解决方案包括:
text
防抖
取消旧请求
请求序列号
只接受最后一次请求结果
例如:
javascript
let requestId = 0;
async function search(keyword) {
const id = ++requestId;
const result = await fetch(`/search?q=${keyword}`);
if (id !== requestId) {
return;
}
render(result);
}
更彻底的方式可以结合:
text
AbortController
取消旧请求。
三十五、Promise 取消问题
30. Promise 能取消吗?
Promise 本身没有通用的取消语义。
例如:
javascript
const p = fetch('/api');
你不能简单:
javascript
p.cancel();
因为 Promise 主要描述的是:
结果状态
而不是:
底层任务生命周期。
因此要取消真正的异步任务,需要任务本身支持取消。
浏览器里最典型的是:
javascript
const controller = new AbortController();
fetch('/api', {
signal: controller.signal
});
controller.abort();
所以必须区分:
text
Promise
→ 结果抽象
AbortController
→ 取消信号
fetch
→ 真正执行任务
这是高级面试非常重要的抽象边界。
三十六、Promise 内存问题
31. Promise 会造成内存泄漏吗?
准确说:
Promise 本身不是天然的内存泄漏源,但长期 pending 的 Promise + 持续注册的回调可能导致对象长期无法释放。
例如:
javascript
const pending = new Promise(() => {});
for (let i = 0; i < 10000; i++) {
pending.then(() => {
console.log(i);
});
}
如果这个 Promise 永远 pending:
text
pending Promise
│
├── callback 1
├── callback 2
├── callback 3
└── ...
这些回调会一直挂在 Promise 上。
MDN 也特别指出,某些组合方法在长期 pending Promise 上反复注册 handler,可能导致 handler 累积;Promise 本身没有提供通用的"取消这些 handler"机制,因此更应该取消底层操作。(MDN Web Docs)
三十七、Promise 最容易出现的几个"错误理解"
32. 高级面试一定要主动纠正这些说法
错误 1
Promise 就是异步。
不准确。
正确:
Promise 是异步结果的抽象;Promise executor 本身可以同步执行。
错误 2
Promise.all 会取消其他请求。
错误。
正确:
Promise.all 只会让聚合 Promise 提前 rejected,不会自动取消其他任务。
错误 3
Promise.race 谁最快就返回谁。
不完整。正确:
谁最先 settled 谁赢,包括 fulfilled 和 rejected。
错误 4
Promise.any 谁最快就返回谁。
错误。正确:
谁最先 fulfilled 谁赢;只有全部 rejected 才失败。
错误 5
await 会阻塞线程。
错误。正确:
await 暂停当前 async 函数的后续执行,不会阻塞 JavaScript 主线程。
错误 6
Promise 就是并行。
错误。正确:
Promise 可以组织异步并发,但 Promise 本身不等于 CPU 并行。
三十八、Promise 高频题优先级
如果你准备的是高级前端岗位,我建议不要平均用力。
S级:必须达到"能原理 + 能手写 + 能追问"
text
★★★★★
1. Promise 三种状态
2. then 为什么返回新 Promise
3. then 返回普通值 / Promise / throw
4. Promise Resolution Procedure
5. resolve(Promise) 为什么会跟随状态
6. Promise.resolve
7. Promise 链
8. Promise 错误传播
9. 微任务
10. async / await
11. Promise.all
12. all / race / allSettled / any
13. Promise.race + 超时
14. Promise 是否可以取消
15. 手写 Promise
A级:大厂非常容易继续追问
text
★★★★☆
16. Promise.all 是否取消其他任务
17. Promise.all 结果为什么按输入顺序
18. await 串行与并发
19. Promise.any + AggregateError
20. thenable
21. finally
22. 未处理 Promise rejection
23. 请求竞态
24. Promise 并发控制
25. Promise 重试
26. AbortController
B级:高级岗位用于拉开差距
text
★★★☆☆
27. Promise 长期 pending 的资源问题
28. Promise.race 的 handler 累积
29. 取消与 Promise 状态的边界
30. 幂等性与 Promise 重试
31. 请求超时与真正取消的区别
32. 微任务过多导致任务饥饿
33. async/await 错误边界
34. 自定义 Promise / thenable 兼容
35. Promise 链循环引用
三十九、最后给你一套真正适合 高级前端的"满分答案"
如果面试官问:
"你系统讲一下 Promise。"
不要从:
"Promise 有三种状态......"
机械开始。建议直接这样回答:
Promise 本质上是对异步操作最终结果的统一抽象,核心解决的是异步结果的传递、组合和错误处理,而不是简单地把代码变成异步。
我通常从四个层面理解 Promise:
第一,状态。
Promise 有 pending、fulfilled、rejected 三种状态,并且状态只能从 pending 转换到最终状态,不能逆转。
第二,链式传递。
then() 每次都会返回一个新的 Promise,所以可以形成 Promise 链。而 then 回调的结果会决定这个新 Promise:
- 返回普通值 → 新 Promise fulfilled
- 返回 Promise / thenable → 新 Promise 跟随它的最终状态
- 抛出异常 → 新 Promise rejected
这其实就是 Promise 链能够不断传递结果和错误的核心。
第三,异步调度。
then 回调不会同步执行,而是进入微任务队列。因此 Promise 经常和 Event Loop 一起考。
async/await 也是建立在 Promise 之上的语法形式,await 只会暂停当前 async 函数,不会阻塞整个 JavaScript 主线程。
第四,Promise 的组合。
常见的四个静态方法分别解决不同问题:
text
Promise.all
→ 全部成功才成功
Promise.race
→ 第一个 settled 的结果
Promise.allSettled
→ 等全部结束,并收集所有结果
Promise.any
→ 第一个成功的结果
但这里有一个容易被忽略的点:这些方法解决的是结果组合,不等于底层任务取消。
例如 Promise.all() 某个任务失败后,其他请求并不会自动取消;Promise.race() 超时后,输掉竞争的请求也不会自动停止。如果业务需要真正取消底层任务,需要使用任务本身提供的取消能力,例如 AbortController。
所以从工程角度看,我会把 Promise 理解成:
text
异步任务
↓
Promise:统一表示最终结果
↓
then / catch / finally:处理结果
↓
all / race / allSettled / any:组合多个结果
↓
async / await:让 Promise 链更接近同步代码的写法
↓
AbortController / 并发控制 / 重试:
解决真正的工程问题
如果继续深入 Promise 的底层实现,核心就是三个东西:
text
状态管理
+
回调队列
+
Promise Resolution Procedure
其中最关键的是 Promise Resolution Procedure,因为它决定了 resolve(普通值)、resolve(Promise)、resolve(thenable) 以及 then 返回不同结果时,新的 Promise 到底应该进入什么状态。
所以我认为 Promise 真正需要掌握的不是 API 背诵,而是:
"状态如何流转 → 结果如何传递 → 异常如何传播 → 多个异步任务如何组合 → 底层任务如何取消和控制。"
把这条链路理解清楚,Promise 绝大多数面试题其实都是它的不同应用。
最后压缩成一张"面试脑图"
text
Promise
│
┌──────────────┼──────────────┐
↓ ↓ ↓
状态 链式调用 并发组合
│ │ │
pending then all
↓ ↓ race
fulfilled 普通值/Promise allSettled
rejected /throw any
│ │
└──────┬───────┘
↓
Promise Resolution
│
┌────────┼────────┐
↓ ↓ ↓
value Promise thenable
│
↓
跟随最终状态
↓
Event Loop
│
↓
Microtask
│
↓
async / await
↓
工程问题
┌───────────┼───────────┐
↓ ↓ ↓
并发控制 超时 取消
↓ ↓ ↓
任务池 race AbortController
│
└────→ 重试 / 竞态 / 幂等性
如果按高级前端的要求准备,Promise 最值得深挖的其实不是再背几十道 API 题,而是下面这条追问链:
Promise 是什么 → 为什么需要状态 →
then为什么返回新 Promise →return Promise为什么会跟随状态 → thenable 是什么 → Promise Resolution Procedure 怎么处理 → 为什么 then 是微任务 → async/await 怎么建立在 Promise 上 →all/race/any/allSettled如何组合 → 为什么组合不等于取消 → 如何做超时/取消/并发控制/重试/竞态处理 → 手写 Promise。
这条链打通,才真正接近高级前端 的 Promise 水平。 (MDN Web Docs)