Promise 全解析:从三状态到 Promise.all

开篇

Promise 是前端开发绕不开的基础概念,几乎每个异步场景都会用到它。

你可以把它理解成一句话:

Promise 是一个"将来会有结果"的承诺。

这个结果现在可能还没返回,所以它先处于等待(pending)状态;等异步任务结束后,要么成功(fulfilled),要么失败(rejected)。

  • Promise 有哪几种状态?
  • Promise.all 到底解决了什么问题?

带着这两个问题,我们一步步拆解。

一、Promise 的三个状态

Promise 一共三种状态,如下图所示:

text 复制代码
pending    待处理(初始状态)
fulfilled  已成功(最终状态)
rejected   已失败(最终状态)

刚创建的 Promise 默认是 pending

js 复制代码
const p = new Promise(() => {});
console.log(p); // Promise { <pending> }

当异步任务成功时,它会变为 fulfilled

js 复制代码
const p = Promise.resolve("成功");
console.log(p); // Promise { <fulfilled>: "成功" }

当异步任务失败时,它会变为 rejected

js 复制代码
const p = Promise.reject("失败");
console.log(p); // Promise { <rejected>: "失败" }

这里有一条核心规则:

Promise 的状态一旦改变,就不可逆转。

也就是说,Promise 只能从 pendingfulfilled,或从 pendingrejected。一旦进入成功或失败状态,结果就已定格,无法再回到 pending,也不能从成功再变为失败。

二、为什么需要 Promise.all

来看一个典型场景:页面需要同时展示一句随机名言和一张随机图片,两者来自不同的接口,互不依赖。

js 复制代码
const getStory = async () =>
    fetch("https://v1.hitokoto.cn/?c=i&encode=json");

const getRATP = async () =>
    fetch("https://api.1314.cool/bingimg/?type=json&rand=1");
  • 获取名言不需要等图片回来
  • 获取图片也不需要等名言回来

既然二者没有依赖关系,就不应该串行等待,否则会浪费宝贵的网络时间。

三、串行写法:能用,但慢

如果写成这样:

js 复制代码
async function main() {
    const story = await getStory();
    const ratp = await getRATP();
}

执行顺序是:

text 复制代码
1. 发起 story 请求
2. 等待 story 返回
3. 再发起 ratp 请求
4. 等待 ratp 返回

这就是串行。假设每个接口耗时 1 秒,总耗时就是 2 秒。

但这两个请求明明可以同时进行,串行白白浪费了一半时间。

四、并发写法:Promise.all 闪亮登场

更好的方式是用 Promise.all 并发执行:

js 复制代码
Promise.all([getStory(), getRATP()]);

这行代码的含义是:

同时发起所有异步任务,等它们全部成功后,再一起返回结果。

完整代码(采用链式写法):

js 复制代码
const getStory = async () =>
    fetch("https://v1.hitokoto.cn/?c=i&encode=json");

const getRATP = async () =>
    fetch("https://api.1314.cool/bingimg/?type=json&rand=1");

async function main() {
    Promise.all([getStory(), getRATP()])
        .then((responses) => {
            // responses 是 [Response, Response]
            return Promise.all(responses.map((res) => res.json()));
        })
        .then((result) => {
            console.log(result); // [名言数据, 图片数据]
        })
        .catch((error) => {
            console.error("请求失败:", error);
        });
}

main();

⚠ 注意大小写:getRATP 不要写成 getRatp,JavaScript 是严格区分大小写的,这种小错误在面试手写题中很常见。

五、Promise.all 的结果顺序

Promise.all 有一个容易被忽视但很重要的特性:

返回结果的顺序,严格遵循传入数组的顺序,与每个 Promise 完成的先后无关。

例如:

js 复制代码
Promise.all([getStory(), getRATP()])
    .then(([storyResponse, imageResponse]) => {
        console.log(storyResponse);  // 始终是 getStory 的结果
        console.log(imageResponse);  // 始终是 getRATP 的结果
    });

即使图片接口先返回,imageResponse 也一定是数组中的第二个元素。这种有序性在处理多个接口数据并需要对应关系时非常有用。

六、为什么还要再写一层 Promise.all

细心的读者可能会疑惑:

明明已经用了 Promise.all,为什么还要再写一个 Promise.all

关键在于 fetch 的行为:

  • fetch 返回的是一个 Response 对象,而不是最终的 JSON 数据。
  • 要读取响应体,需要调用 res.json(),而 res.json() 本身也返回一个 Promise

所以,完整的流程其实是两层并发:

  • 第一层 Promise.all:并发请求两个接口,拿到 Response 对象数组。
  • 第二层 Promise.all:并发解析两个 Response 的 JSON 数据。

这种"嵌套 Promise.all"是处理多个 Fetch 请求的经典模式。

七、错误处理:一个失败,整体失败

Promise.all 的错误处理遵循"木桶短板"原则:

只要传入的 Promise 中有一个变为 rejectedPromise.all 返回的 Promise 就会立即变为 rejected,并把这个失败原因传递给 catch

js 复制代码
Promise.all([p1, p2, p3])
    .then((result) => {
        console.log("全部成功", result);
    })
    .catch((error) => {
        console.log("有一个失败了", error);
    });

一旦 p1p2p3 中的任意一个失败,就会立即进入 catch

但有一个容易混淆的点:

Promise.all 变为 rejected,并不意味着其他已经开始的异步任务会被取消。

比如三个请求已经发出,第二个率先失败,那么 Promise.all 会立刻 reject,但第一个和第三个请求仍然会继续执行,只是它们的结果被忽略了。

如果在面试中被问到:

Promise.all 中一个 Promise 失败了,其他 Promise 还会执行吗?

可以这样回答:

如果其他 Promise 已经创建并开始执行,它们不会被自动取消,仍会完成。但 Promise.all 会立即 reject,并将第一个失败原因返回给 catch

这个回答比"不会执行"更加准确。

补充 :如果你需要等待所有 Promise 完成(无论成功或失败),可以使用 Promise.allSettled,它会返回每个 Promise 的最终状态和结果,而不会提前 reject。

八、async/await 写法

上面的链式代码也可以改用 async/await 风格:

js 复制代码
async function main() {
    try {
        const responses = await Promise.all([getStory(), getRATP()]);
        const result = await Promise.all(
            responses.map((res) => res.json())
        );
        console.log(result);
    } catch (error) {
        console.error("请求失败:", error);
    }
}

两种写法本质相同:

text 复制代码
.then().catch()   ←→   async/await + try/catch

选择哪种取决于场景:

  • 逻辑较短时,链式写法清晰。
  • 流程较长或有复杂条件分支时,async/await 更易读。

九、面试怎么答?

如果面试官问:

Promise.all 是什么?

可以这样回答:

Promise.all 接收一个 Promise 数组,并发执行这些异步任务。只有当所有 Promise 都变为 fulfilled 时,它才会返回一个结果数组,顺序与传入数组严格一致。如果其中任何一个变为 rejected,它会立即 reject,并将该失败原因传递给 catch

如果追问:

一个失败了,其他 Promise 还会继续执行吗?

回答:

已经开始执行的 Promise 不会被自动取消,它们仍会运行至完成,只是 Promise.all 已经进入 rejected 状态,不再等待和收集它们的结果。

这两个回答基本涵盖了 Promise.all 的核心考点。

十、总结

知识点 结论
Promise 三状态 pendingfulfilledrejected
状态变化规则 一旦变为成功或失败,就不可逆
Promise.all 用途 并发执行多个互不依赖的异步任务
成功结果 所有 Promise 成功后,返回结果数组,顺序与传入一致
失败规则 任意一个失败,整体立即失败,并返回第一个失败原因
是否取消其他任务 不会自动取消已开始的任务,它们仍会执行,但结果被忽略
替代方案 如需要所有结果(含失败),可使用 Promise.allSettled

Promise 的难点不在于语法,而在于理解异步任务的执行顺序和并发控制。

Promise.all 最核心的价值就是:

把多个互不依赖的异步任务并发执行,并在全部成功后统一处理结果,从而大幅提升性能。

相关推荐
嘟嘟07172 小时前
Node.js spawn 实现迷你 Cursor 自动化终端|子进程完整解析
前端·后端·面试
weedsfly2 小时前
纯函数与副作用——前端开发中最容易被忽视的质量保证
前端·javascript·面试
whaledown2 小时前
互联网大厂Java求职面试三轮提问详解(涵盖Spring Boot、微服务、Kafka等核心技术)
java·jvm·数据库·spring boot·微服务·面试·kafka
程序员爱钓鱼3 小时前
Cargo 包管理器详解:Rust 开发必会工具
后端·面试·rust
大江东去浪淘尽千古风流人物3 小时前
【SLAM】slam高动态位姿估计全链路拆解
算法·面试·职场和发展·视觉里程计·slam·vio·大疆
我叫黑大帅13 小时前
MySQL 并发插入竞态问题:原子写入实践指南
后端·mysql·面试
我叫黑大帅15 小时前
别再手动写 updated_at了,这坑我踩过
后端·面试·go
ShineWinsu15 小时前
对于Linux:网络基础的解析
linux·网络·面试·udp·笔试·ip·tcp
咖啡星人k16 小时前
从Vibe Coding到Prompt Engineering:AI编程的新技能树
面试·职场和发展·ai编程