6 道 Promise 实战题,从入门到精通,彻底吃透异步流程

29 阅读12分钟

很多前端小白在刚学习Promise都难理解异步这一过程,下面我出了六道题目,经过这六道题目的练习,我相信你能很快掌握Promise中异步的过程,以及,什么是Promise?

第一部分: Promise 核心知识点速览

先来看看Promise的知识点来复习一遍

  1. 3 个核心状态pending(等待)→ fulfilled(成功)/ rejected(失败),一旦切换不可逆(比如调用resolve后,再调用reject无效);

  2. 3 个基础方法

    • then:接收成功回调,返回新 Promise;
    • catch:捕获前面所有then或 Promise 本身的错误,返回新 Promise;
    • finally:无论成功失败都执行,不依赖状态,不改变最终结果;
  3. 4 个关键静态方法

    • Promise.resolve(data):快速创建 “已成功” 的 Promise;
    • Promise.reject(error):快速创建 “已失败” 的 Promise;
    • Promise.all(arr):并行执行 Promise 数组,全部成功才返回结果数组(顺序与输入一致),一个失败则立即返回失败;
    • Promise.race(arr):并行执行 Promise 数组,“第一个决议”(成功或失败)的结果就是最终结果;
  4. 1 个核心特性then/catch会返回新的 Promise,支持链式调用,错误会 “冒泡传递”(前面的错误会被后面最近的catch捕获)。

  5. 1 个语法糖:async/await(基于 Promise 实现,简化异步代码):

    • async:修饰函数,函数执行后必然返回 Promise(同步返回值会被Promise.resolve包装,抛出的错误会被Promise.reject包装);
    • await:只能在async函数内使用,用于 “等待 Promise 决议”(阻塞当前函数执行,不阻塞全局),直接获取 Promise 的成功结果;
    • 错误处理:await后的 Promise 失败时,需用try/catch捕获(等价于 Promise 的catch);
    • 本质:async/awaitthen链式调用的简化写法,底层依然依赖 Promise 的状态机制

第二部分: 6 道 Promise 实战题(含详细解析)

题目1:基础 - 异步随机数生成器(Promise + async/await)

题目描述

实现一个异步函数 generateRandomNumber(min, max, failRate),满足:

  1. 延迟 500ms 后执行(模拟异步操作);
  2. 成功时返回 [min, max] 之间的随机整数(包含边界);
  3. 失败概率由 failRate 控制(0~1之间,比如 failRate=0.3 表示30%概率失败),失败时抛出错误 new Error("随机数生成失败")
  4. 分别用 .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();

解析

  1. 函数内部用 setTimeout 模拟异步延迟,通过 Math.random() 模拟失败概率;
  2. 成功时用 resolve 传递随机数,失败时用 reject 传递错误;
  3. 两种调用方式等价: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
第四步捕获错误:手动触发错误

解析

  1. 执行顺序:

    • 先执行 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无错误可捕获,不执行。
  2. 关键规则:

    • 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. 需求1:并行请求3个接口,所有接口成功才返回结果(失败则立即抛出错误);
  2. 需求2:并行请求3个接口,哪个先完成就返回哪个的结果(无论成功失败);
  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. 需求1(Promise.all):

    • 只要有一个接口失败(fetchAddress),立即触发catch,返回第一个错误原因,符合“全成功才返回”场景(如表单提交前需验证多个接口)。
  2. 需求2(Promise.race):

    • fetchAddress 150ms完成(失败),是3个中最快的,所以直接返回失败结果,符合“超时控制”“优先使用最快接口”场景。
  3. 需求3(Promise.allSettled):

    • 等待所有接口完成(fetchUser 200ms、fetchOrders 300ms、fetchAddress 150ms),返回3个状态对象数组,每个对象包含 status(fulfilled/rejected)和 value/reason
    • 适合“不依赖所有接口成功,需要统计所有结果”场景(如数据统计、批量操作日志)。

易错点

  • 用Promise.all时忽略“短路特性”(一个失败就整体失败);
  • 认为Promise.race只返回成功结果(实际失败也会优先返回);
  • 混淆allSettled的结果格式(每个元素是状态对象,需通过status筛选)。

题目4:进阶 - 依赖异步的顺序+并行组合

题目描述

实现一个异步流程,需求如下:

  1. 先调用接口1(fetchToken)获取用户令牌(token),耗时200ms;
  2. 拿到token后,并行调用接口2(fetchUserInfo)和接口3(fetchUserAssets),两个接口都需要token作为参数;
    • 接口2:耗时300ms,返回用户信息;
    • 接口3:耗时250ms,返回用户资产;
  3. 等待两个并行接口完成后,合并结果(用户信息+用户资产)并返回;
  4. 任何一个接口失败,都需要捕获错误并输出“操作失败”。

考点

  • 异步依赖的顺序执行(先拿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));

解析

  1. 核心逻辑:

    • 第一步用 await fetchToken() 保证“先拿token,再调后续接口”(顺序执行);
    • 第二步用 Promise.all 并行调用两个接口,避免顺序执行导致耗时增加(300ms vs 300+250=550ms,提升性能);
    • try/catch 捕获所有环节的错误(token获取失败、任一并行接口失败)。
  2. 性能优化点:

    • 无依赖的异步操作尽量用 Promise.all 并行执行,减少总耗时;
    • 有依赖的异步操作必须顺序执行(如token→需要token的接口)。

易错点

  • 把并行接口写成顺序执行(await fetchUserInfo(token); await fetchUserAssets(token)),导致性能浪费;
  • 忘记给并行接口传递token参数(异步函数的参数传递需要显式处理);
  • 错误捕获不完整(如只捕获token获取的错误,未捕获并行接口的错误)。

题目5:进阶 - 异步重试机制(Promise状态+循环)

题目描述

实现一个异步重试函数 retryAsync(fn, maxRetries, delay),满足:

  1. fn:需要执行的异步函数(返回Promise);
  2. maxRetries:最大重试次数(如3次表示:执行1次+重试3次,共4次);
  3. delay:每次重试的延迟时间(毫秒);
  4. 逻辑:
    • 执行 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));

解析

  1. 核心逻辑:

    • while 循环控制尝试次数(attempt 从0到 maxRetries);
    • 每次循环中执行 fn(),成功则返回结果;失败则判断是否还有重试次数;
    • 有重试次数则等待 delay 毫秒(通过 sleep 函数),然后继续循环;
    • 无重试次数则抛出最终错误。
  2. 关键设计:

    • 循环比递归更适合重试场景(避免递归深度过大导致栈溢出);
    • sleep 函数保证重试前的延迟,符合实际场景(避免高频重试压垮接口);
    • 每次尝试都有日志,便于调试。

易错点

  • 重试次数计算错误(如 maxRetries=3 实际执行4次,需注意 attempt 的边界);
  • 忘记在重试前加延迟(导致高频重试,不符合实际需求);
  • 递归实现时未处理递归终止条件(导致无限递归)。

题目 6:进阶 - 串行执行异步任务(掌握 Promise 串行流程)

要求

给定一个异步任务数组 tasks,每个任务是「延迟 1 秒后打印当前索引」(如任务 0 打印 任务 0 执行完成),要求:

  1. 按顺序执行任务(任务 0 完成后,再执行任务 1,依次类推)
  2. 所有任务执行完毕后,打印「全部任务执行完成」
  3. 禁止使用 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 核心考点串联

  1. 状态不可逆:pending → fulfilled/rejected 一旦切换,后续操作无效

  2. 链式调用:then/catch 返回新 Promise,支持值传递,错误冒泡(catch 捕获前面所有错误)

  3. 静态方法:

    • resolve/reject:快速创建 Promise
    • all:并行 + 快速失败(全部成功才返回)
    • allSettled:并行 + 等待所有完成(返回每个任务状态)
    • race:竞速(第一个完成的结果)
    • any:竞速(第一个成功的结果,所有失败才返回失败)
  4. 流程控制:并行用 all/allSettled,串行用 reduce 链式或 async/await

把这 6 道题亲手写一遍,理解每一步的原理,Promise 就能彻底掌握!如果某道题卡壳,建议先回顾对应的核心知识点,再逐步调试~