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

37 阅读6分钟

开篇

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

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

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

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

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

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

一、Promise 的三个状态

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

image.png

pending    待处理(初始状态)
fulfilled  已成功(最终状态)
rejected   已失败(最终状态)

刚创建的 Promise 默认是 pending

const p = new Promise(() => {});
console.log(p); // Promise { <pending> }

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

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

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

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

这里有一条核心规则:

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

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

二、为什么需要 Promise.all

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

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() {
    const story = await getStory();
    const ratp = await getRATP();
}

执行顺序是:

1. 发起 story 请求
2. 等待 story 返回
3. 再发起 ratp 请求
4. 等待 ratp 返回

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

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

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

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

Promise.all([getStory(), getRATP()]);

这行代码的含义是:

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

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

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 完成的先后无关。

例如:

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

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

image.png

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

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

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

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

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

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 风格:

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);
    }
}

两种写法本质相同:

.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 最核心的价值就是:

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