很多前端小白在刚学习Promise都难理解异步这一过程,下面我出了六道题目,经过这六道题目的练习,我相信你能很快掌握Promise中异步的过程,以及,什么是Promise?
第一部分: Promise 核心知识点速览
先来看看Promise的知识点来复习一遍
-
3 个核心状态:
pending(等待)→fulfilled(成功)/rejected(失败),一旦切换不可逆(比如调用resolve后,再调用reject无效); -
3 个基础方法:
then:接收成功回调,返回新 Promise;catch:捕获前面所有then或 Promise 本身的错误,返回新 Promise;finally:无论成功失败都执行,不依赖状态,不改变最终结果;
-
4 个关键静态方法:
Promise.resolve(data):快速创建 “已成功” 的 Promise;Promise.reject(error):快速创建 “已失败” 的 Promise;Promise.all(arr):并行执行 Promise 数组,全部成功才返回结果数组(顺序与输入一致),一个失败则立即返回失败;Promise.race(arr):并行执行 Promise 数组,“第一个决议”(成功或失败)的结果就是最终结果;
-
1 个核心特性:
then/catch会返回新的 Promise,支持链式调用,错误会 “冒泡传递”(前面的错误会被后面最近的catch捕获)。 -
1 个语法糖:async/await(基于 Promise 实现,简化异步代码):
async:修饰函数,函数执行后必然返回 Promise(同步返回值会被Promise.resolve包装,抛出的错误会被Promise.reject包装);await:只能在async函数内使用,用于 “等待 Promise 决议”(阻塞当前函数执行,不阻塞全局),直接获取 Promise 的成功结果;- 错误处理:
await后的 Promise 失败时,需用try/catch捕获(等价于 Promise 的catch); - 本质:
async/await是then链式调用的简化写法,底层依然依赖 Promise 的状态机制
第二部分: 6 道 Promise 实战题(含详细解析)
题目1:基础 - 异步随机数生成器(Promise + async/await)
题目描述
实现一个异步函数 generateRandomNumber(min, max, failRate),满足:
- 延迟 500ms 后执行(模拟异步操作);
- 成功时返回
[min, max]之间的随机整数(包含边界); - 失败概率由
failRate控制(0~1之间,比如failRate=0.3表示30%概率失败),失败时抛出错误new Error("随机数生成失败"); - 分别用 .then()/.catch() 和 async/await + try/catch 两种方式调用该函数。
考点
- Promise 基本创建(resolve/reject);
- async/await 语法使用;
- 异步错误捕获(catch/try/catch)。
参考答案
// 实现异步随机数生成函数
function generateRandomNumber(min, max, failRate) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟失败概率:Math.random()返回0~1的随机数
if (Math.random() < failRate) {
reject(new Error("随机数生成失败"));
} else {
// 生成[min, max]之间的随机整数
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
resolve(randomNum);
}
}, 500);
});
}
// 方式1:.then()/.catch() 调用
console.log("方式1开始");
generateRandomNumber(1, 100, 0.3)
.then((num) => console.log("方式1成功:", num))
.catch((err) => console.log("方式1失败:", err.message))
.finally(() => console.log("方式1结束"));
// 方式2:async/await + try/catch 调用
async function testRandom() {
console.log("方式2开始");
try {
const num = await generateRandomNumber(1, 100, 0.3);
console.log("方式2成功:", num);
} catch (err) {
console.log("方式2失败:", err.message);
} finally {
console.log("方式2结束");
}
}
testRandom();
解析
- 函数内部用
setTimeout模拟异步延迟,通过Math.random()模拟失败概率; - 成功时用
resolve传递随机数,失败时用reject传递错误; - 两种调用方式等价:
async/await是 Promise 的语法糖,try/catch对应.catch(),finally无论成功失败都执行。
易错点
- 忘记
setTimeout导致同步执行(异步函数必须包裹异步操作); - 生成随机数时忽略
+1(导致无法取到max值,正确公式:Math.floor(Math.random() * (max - min + 1)) + min); - async 函数中未用
try/catch捕获错误(导致未处理的拒绝警告)。
题目2:中级 - 链式调用的值传递与错误冒泡
题目描述
分析以下代码的执行结果,并用文字说明执行顺序和原因:
function asyncAdd(a, b) {
return new Promise((resolve) => {
setTimeout(() => resolve(a + b), 100);
});
}
asyncAdd(1, 2)
.then((res) => {
console.log("第一步结果:", res);
return res * 2; // 返回普通值,自动包装为Promise.resolve(6)
})
.then((res) => {
console.log("第二步结果:", res);
return asyncAdd(res, 3); // 返回Promise,等待其完成
})
.then((res) => {
console.log("第三步结果:", res);
throw new Error("手动触发错误"); // 抛出错误,状态变为rejected
})
.then(
(res) => console.log("第四步成功:", res), // 不执行
(err) => {
console.log("第四步捕获错误:", err.message);
return 100; // 错误处理后返回值,状态恢复为fulfilled
}
)
.catch((err) => console.log("最终捕获错误:", err.message)); // 不执行
考点
- Promise 链式调用的「值传递规则」(then返回值自动包装为Promise);
- 错误冒泡机制(错误会传递到最近的catch/then的第二个参数);
- then的第二个参数与catch的区别(前者只捕获前一个then的错误,后者捕获所有前置错误)。
参考答案(执行结果)
第一步结果:3
第二步结果:6
第三步结果:9
第四步捕获错误:手动触发错误
解析
-
执行顺序:
- 先执行
asyncAdd(1,2),100ms后resolve(3),触发第一个then; - 第一个then返回3*2=6(普通值),自动包装为Promise.resolve(6),触发第二个then;
- 第二个then返回
asyncAdd(6,3)(Promise),100ms后resolve(9),触发第三个then; - 第三个then抛出错误,状态变为rejected,跳过后续成功回调(第四个then的第一个参数);
- 第四个then的第二个参数捕获错误,返回100(状态恢复为fulfilled),后续catch无错误可捕获,不执行。
- 先执行
-
关键规则:
- then的返回值(无论普通值还是Promise)会作为下一个then的输入;
- 错误会“冒泡”:只要前置环节抛出错误,会跳过所有后续的成功回调,直到被catch或then的第二个参数捕获;
- 若错误被捕获后返回了值(如第四个then返回100),后续链式调用会恢复为fulfilled状态。
易错点
- 认为“抛出错误后整个链式都会中断”(实际被捕获后可恢复);
- 混淆then的第二个参数和catch(前者只捕获前一个then的错误,后者捕获所有前置错误,推荐用catch统一捕获)。
题目3:中级 - Promise静态方法实战(all/race/allSettled)
题目描述
有3个异步接口模拟函数,需求如下:
// 接口1:200ms返回用户信息
const fetchUser = () => Promise.resolve({ id: 1, name: "张三" });
// 接口2:300ms返回用户订单
const fetchOrders = () => new Promise((resolve) => setTimeout(() => resolve([{ id: 101, price: 100 }]), 300));
// 接口3:150ms返回错误(模拟接口失败)
const fetchAddress = () => Promise.reject(new Error("地址接口维护中"));
请实现3个需求:
- 需求1:并行请求3个接口,所有接口成功才返回结果(失败则立即抛出错误);
- 需求2:并行请求3个接口,哪个先完成就返回哪个的结果(无论成功失败);
- 需求3:并行请求3个接口,等待所有接口完成(无论成功失败),最后返回所有接口的状态和结果。
考点
- Promise.all(全成功才返回,短路失败);
- Promise.race(竞速,先完成者优先);
- Promise.allSettled(等待所有完成,返回所有状态)。
参考答案
// 需求1:所有接口成功才返回(Promise.all)
console.log("需求1开始");
Promise.all([fetchUser(), fetchOrders(), fetchAddress()])
.then(([user, orders, address]) => {
console.log("需求1成功:", { user, orders, address });
})
.catch((err) => console.log("需求1失败:", err.message)); // 输出:地址接口维护中
// 需求2:先完成的接口先返回(Promise.race)
console.log("需求2开始");
Promise.race([fetchUser(), fetchOrders(), fetchAddress()])
.then((result) => console.log("需求2成功:", result)) // 不执行(fetchAddress先失败)
.catch((err) => console.log("需求2失败:", err.message)); // 输出:地址接口维护中(150ms后)
// 需求3:所有接口完成后返回所有状态(Promise.allSettled)
console.log("需求3开始");
Promise.allSettled([fetchUser(), fetchOrders(), fetchAddress()])
.then((results) => {
console.log("需求3结果:", results);
// 筛选成功的结果
const successResults = results
.filter((item) => item.status === "fulfilled")
.map((item) => item.value);
console.log("需求3成功结果:", successResults);
});
解析
-
需求1(Promise.all):
- 只要有一个接口失败(fetchAddress),立即触发catch,返回第一个错误原因,符合“全成功才返回”场景(如表单提交前需验证多个接口)。
-
需求2(Promise.race):
- fetchAddress 150ms完成(失败),是3个中最快的,所以直接返回失败结果,符合“超时控制”“优先使用最快接口”场景。
-
需求3(Promise.allSettled):
- 等待所有接口完成(fetchUser 200ms、fetchOrders 300ms、fetchAddress 150ms),返回3个状态对象数组,每个对象包含
status(fulfilled/rejected)和value/reason; - 适合“不依赖所有接口成功,需要统计所有结果”场景(如数据统计、批量操作日志)。
- 等待所有接口完成(fetchUser 200ms、fetchOrders 300ms、fetchAddress 150ms),返回3个状态对象数组,每个对象包含
易错点
- 用Promise.all时忽略“短路特性”(一个失败就整体失败);
- 认为Promise.race只返回成功结果(实际失败也会优先返回);
- 混淆allSettled的结果格式(每个元素是状态对象,需通过status筛选)。
题目4:进阶 - 依赖异步的顺序+并行组合
题目描述
实现一个异步流程,需求如下:
- 先调用接口1(
fetchToken)获取用户令牌(token),耗时200ms; - 拿到token后,并行调用接口2(
fetchUserInfo)和接口3(fetchUserAssets),两个接口都需要token作为参数;- 接口2:耗时300ms,返回用户信息;
- 接口3:耗时250ms,返回用户资产;
- 等待两个并行接口完成后,合并结果(用户信息+用户资产)并返回;
- 任何一个接口失败,都需要捕获错误并输出“操作失败”。
考点
- 异步依赖的顺序执行(先拿token,再调后续接口);
- 顺序+并行的组合(先顺序,后并行);
- async/await 与 Promise.all 的结合;
- 全局错误捕获。
参考答案
// 模拟接口函数
const fetchToken = () => new Promise((resolve) => setTimeout(() => resolve("token_123456"), 200));
const fetchUserInfo = (token) => new Promise((resolve) => {
setTimeout(() => resolve({ token, name: "李四", age: 25 }), 300);
});
const fetchUserAssets = (token) => new Promise((resolve) => {
setTimeout(() => resolve({ token, balance: 1000, assets: ["手机", "电脑"] }), 250);
});
// 实现异步流程
async function getUserData() {
try {
// 第一步:顺序执行,先获取token(依赖前置结果)
const token = await fetchToken();
console.log("获取到token:", token);
// 第二步:并行执行,两个接口都依赖token,无相互依赖
const [userInfo, userAssets] = await Promise.all([
fetchUserInfo(token),
fetchUserAssets(token)
]);
// 第三步:合并结果
return {
userInfo,
userAssets,
combined: {
name: userInfo.name,
balance: userAssets.balance,
totalAssets: userAssets.assets.length
}
};
} catch (err) {
console.log("操作失败:", err.message);
throw err; // 可选择抛出错误,让调用方进一步处理
}
}
// 调用函数
getUserData().then((data) => console.log("最终结果:", data));
解析
-
核心逻辑:
- 第一步用
await fetchToken()保证“先拿token,再调后续接口”(顺序执行); - 第二步用
Promise.all并行调用两个接口,避免顺序执行导致耗时增加(300ms vs 300+250=550ms,提升性能); try/catch捕获所有环节的错误(token获取失败、任一并行接口失败)。
- 第一步用
-
性能优化点:
- 无依赖的异步操作尽量用
Promise.all并行执行,减少总耗时; - 有依赖的异步操作必须顺序执行(如token→需要token的接口)。
- 无依赖的异步操作尽量用
易错点
- 把并行接口写成顺序执行(
await fetchUserInfo(token); await fetchUserAssets(token)),导致性能浪费; - 忘记给并行接口传递token参数(异步函数的参数传递需要显式处理);
- 错误捕获不完整(如只捕获token获取的错误,未捕获并行接口的错误)。
题目5:进阶 - 异步重试机制(Promise状态+循环)
题目描述
实现一个异步重试函数 retryAsync(fn, maxRetries, delay),满足:
fn:需要执行的异步函数(返回Promise);maxRetries:最大重试次数(如3次表示:执行1次+重试3次,共4次);delay:每次重试的延迟时间(毫秒);- 逻辑:
- 执行
fn,若成功则返回结果; - 若失败,等待
delay毫秒后重试,直到重试次数用完; - 所有次数都失败,则抛出最终错误。
- 执行
考点
- Promise 状态判断与循环结合;
- async/await 循环执行异步函数;
- 延迟重试的实现(setTimeout + Promise);
- 递归/循环的边界处理(重试次数递减)。
参考答案
// 延迟函数(复用之前的sleep逻辑)
function sleep(millis) {
return new Promise(resolve => setTimeout(resolve, millis));
}
// 重试函数实现(循环方式,比递归更易理解)
async function retryAsync(fn, maxRetries, delay) {
let attempt = 0; // 当前尝试次数
while (attempt <= maxRetries) {
try {
console.log(`第${attempt + 1}次尝试执行`);
const result = await fn(); // 执行异步函数
console.log("执行成功!");
return result; // 成功则返回结果,终止循环
} catch (err) {
attempt++; // 尝试次数+1
// 若重试次数用完,抛出最终错误
if (attempt > maxRetries) {
console.log(`所有${maxRetries + 1}次尝试均失败`);
throw new Error(`最终失败:${err.message}`);
}
// 重试前延迟
console.log(`第${attempt}次失败,${delay}ms后重试...`);
await sleep(delay);
}
}
}
// 测试:模拟一个50%概率成功的异步函数
function randomSuccessFn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve("执行成功的结果") : reject(new Error("随机失败"));
}, 100);
});
}
// 调用重试函数:最大重试3次,每次延迟500ms
retryAsync(randomSuccessFn, 3, 500)
.then((res) => console.log("最终结果:", res))
.catch((err) => console.log("捕获最终错误:", err.message));
解析
-
核心逻辑:
- 用
while循环控制尝试次数(attempt从0到maxRetries); - 每次循环中执行
fn(),成功则返回结果;失败则判断是否还有重试次数; - 有重试次数则等待
delay毫秒(通过sleep函数),然后继续循环; - 无重试次数则抛出最终错误。
- 用
-
关键设计:
- 循环比递归更适合重试场景(避免递归深度过大导致栈溢出);
sleep函数保证重试前的延迟,符合实际场景(避免高频重试压垮接口);- 每次尝试都有日志,便于调试。
易错点
- 重试次数计算错误(如
maxRetries=3实际执行4次,需注意attempt的边界); - 忘记在重试前加延迟(导致高频重试,不符合实际需求);
- 递归实现时未处理递归终止条件(导致无限递归)。
题目 6:进阶 - 串行执行异步任务(掌握 Promise 串行流程)
要求
给定一个异步任务数组 tasks,每个任务是「延迟 1 秒后打印当前索引」(如任务 0 打印 任务 0 执行完成),要求:
- 按顺序执行任务(任务 0 完成后,再执行任务 1,依次类推)
- 所有任务执行完毕后,打印「全部任务执行完成」
- 禁止使用
async/await(先用 Promise 原生实现,再拓展async/await写法)
提示
- 串行的核心:用
reduce遍历任务数组,将前一个任务的then与下一个任务关联 - 初始值为
Promise.resolve()(一个已成功的 Promise,作为链式调用的起点)
解答
方法 1:Promise 原生(reduce 链式)
// 定义异步任务数组:每个任务延迟1秒,打印索引
const tasks = [
(index) => new Promise((resolve) => setTimeout(() => {
console.log(`任务 ${index} 执行完成`);
resolve();
}, 1000)),
(index) => new Promise((resolve) => setTimeout(() => {
console.log(`任务 ${index} 执行完成`);
resolve();
}, 1000)),
(index) => new Promise((resolve) => setTimeout(() => {
console.log(`任务 ${index} 执行完成`);
resolve();
}, 1000)),
];
// 串行执行:reduce 构建链式调用
tasks.reduce((prevPromise, currentTask, index) => {
// 前一个任务完成后,执行当前任务
return prevPromise.then(() => currentTask(index));
}, Promise.resolve()) // 初始值:已成功的 Promise
.then(() => console.log("全部任务执行完成"));
方法 2:async/await 写法(拓展,更简洁)
async function runTasksSerial(tasks) {
for (let i = 0; i < tasks.length; i++) {
await tasks[i](i); // 等待当前任务完成,再执行下一个
}
console.log("全部任务执行完成");
}
runTasksSerial(tasks);
解析
- 核心:
reduce的作用是「累积 Promise 链式」—— 每一步都返回新的 Promise,确保前一个任务完成后再执行下一个 - 对比并行:如果用
Promise.all(tasks.map((task, i) => task(i))),会同时执行所有任务(3 个任务同时打印,总耗时 1 秒),而串行总耗时 3 秒 async/await是 Promise 的语法糖,本质还是基于 Promise 的状态机制,上面两种写法效果完全一致
总结:Promise 核心考点串联
-
状态不可逆:
pending→fulfilled/rejected一旦切换,后续操作无效 -
链式调用:
then/catch返回新 Promise,支持值传递,错误冒泡(catch捕获前面所有错误) -
静态方法:
resolve/reject:快速创建 Promiseall:并行 + 快速失败(全部成功才返回)allSettled:并行 + 等待所有完成(返回每个任务状态)race:竞速(第一个完成的结果)any:竞速(第一个成功的结果,所有失败才返回失败)
-
流程控制:并行用
all/allSettled,串行用reduce链式或async/await
把这 6 道题亲手写一遍,理解每一步的原理,Promise 就能彻底掌握!如果某道题卡壳,建议先回顾对应的核心知识点,再逐步调试~