跳到正文
前端知识库
JavaScript

13. 说说你对Promise的理解?Promise.all和Promise.race的区别?

理解 Promise 的单次决议、thenable 接管、微任务入队、链式错误传播与并发组合边界。

约 7 分钟JavaScript · Promise · 异步 · 微任务 · 面试

问题解析

Promise是JavaScript异步编程的重要解决方案,理解Promise的原理、状态变化以及静态方法的使用是前端面试中的高频考点。本题主要考察对Promise核心概念的理解以及Promise.all和Promise.race的区别和应用场景。

核心概念

什么是Promise

Promise是ES6引入的异步编程解决方案,它是一个对象,代表了一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:

  • pending(进行中):初始状态,既未完成也未拒绝
  • fulfilled(已成功):操作成功完成
  • rejected(已失败):操作失败

Promise的特点

  1. 状态一旦改变,就不会再变:Promise对象的状态改变只有两种可能:从pending变为fulfilled,或者从pending变为rejected
  2. 链式调用:通过then方法可以进行链式调用,解决回调地狱问题
  3. 错误捕获:通过catch方法可以统一捕获错误

详细解答

Promise基本用法

const promise = new Promise((resolve, reject) => {
  const shouldSucceed = true;
  setTimeout(() => {
    if (shouldSucceed) {
      resolve('完成');
    } else {
      reject(new Error('操作失败'));
    }
  }, 1000);
});

promise
  .then(value => {
    // 成功时的回调
    console.log(value);
  })
  .catch(error => {
    // 失败时的回调
    console.error(error);
  })
  .finally(() => {
    // 无论成功失败都会执行
    console.log('完成');
  });

Promise.all

Promise.all用于将多个Promise实例包装成一个新的Promise实例。

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
  .then(values => {
    console.log(values); // [1, 2, 3]
  })
  .catch(error => {
    console.error(error);
  });

特点:

  • 所有Promise都成功时,返回的Promise才会成功,结果是一个数组,按传入顺序排列
  • 只要有一个Promise失败,整个Promise.all就失败,返回第一个失败的Promise的原因

Promise.race

Promise.race同样是将多个Promise实例包装成一个新的Promise实例,但只要有一个实例率先改变状态,整个Promise的状态就跟着改变。

const p1 = new Promise((resolve) => {
  setTimeout(resolve, 500, 'one');
});
const p2 = new Promise((resolve) => {
  setTimeout(resolve, 1000, 'two');
});

Promise.race([p1, p2])
  .then(value => {
    console.log(value); // 'one',因为p1先完成
  });

特点:

  • 哪个Promise先完成(无论成功或失败),就采用哪个Promise的结果
  • 常用于设置超时处理

Promise.all与Promise.race的区别

特性 Promise.all Promise.race
触发条件 所有Promise完成 第一个Promise完成
成功结果 所有Promise结果的数组 第一个完成的Promise结果
失败条件 任意一个Promise失败 第一个完成的Promise失败
应用场景 并行执行多个异步操作 竞速、超时控制

深入理解

单次决议解决什么问题

回调式接口把后续函数交给提供者,调用方需要担心回调被重复调用、过早同步调用或忘记调用。Promise 通过单次决议和异步 reaction 提供统一消费契约:提供者通知结果,调用方选择如何链接后续操作。同一 Promise 的每次 then 注册对应的处理器最多运行一次,重复 resolve/reject 不会重复触发它。

这不等于业务操作只执行一次。若第三方在调用 resolve 前已经重复扣款,Promise 无法撤销这些副作用;写请求仍需服务端幂等控制。永不调用 resolve/reject 的提供者也仍可让 Promise 一直 pending。

resolve 不一定立即 fulfilled

resolved 表示结果已被锁定,可以是采用另一个尚未完成的 Promise;fulfilled/rejected 才是最终状态。例如先 resolve(pendingPromise) 再 reject(error),后者会被忽略,但外层仍可能暂时 pending。不能只用“状态是否仍是 pending”判断能否再次决议。

Promise.resolve(value) 与 then 返回值的吸收遵循相同的关键规则:

值 如何处理
普通值,或 then 不可调用的对象 以该值成功完成
同构造器的原生 Promise(Promise.resolve) 通常直接返回该实例;不要推广为任意子类/跨 realm 对象都保留引用
带可调用 then 的对象或函数 读取 then,通过 job 调用它并传入 resolve/reject;采用回调给出的结果
读取 then 抛错,或在尚未决议时调用 then 抛错 拒绝 Promise

then 可来自自身属性或原型链;不是采用 then 方法的 return 值。普通函数返回值的规则适用于 then(onFulfilled) 中的处理器,别把两层 then 混为一谈。

const thenable = {
  then(resolve, reject) {
    resolve(42);
    reject(new Error('后续决议被忽略'));
    return 99; // 不会成为结果
  }
};
Promise.resolve(thenable).then(console.log); // 42
Promise.resolve({ then: 1 }).then(console.log); // { then: 1 }

reaction 何时进入微任务队列

new Promise(executor) 同步执行 executor;resolve() 不会同步执行已注册的 then 回调。pending 时调用 then 先保存 reaction;Promise 最终完成后才将对应 reaction 入队。对已经完成的 Promise 调用 then,则在注册时安排 reaction job。没有处理器的 resolve(普通值) 不会凭空生成一个用户回调任务;吸收 thenable 则还可能安排专用 job。

const order = [];
let finish;
const pending = new Promise(resolve => { finish = resolve; });
pending.then(() => order.push('pending reaction'));
queueMicrotask(() => order.push('先入队'));
finish();
Promise.resolve().then(() => order.push('settled reaction'));
order.push('同步');
setTimeout(() => console.log(order), 0);
// ['同步', '先入队', 'pending reaction', 'settled reaction']

Promise的链式调用

fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

每次 then/catch/finally 都返回新 Promise。then 处理器返回普通值会使下游成功,抛错会使下游拒绝,返回 Promise/thenable 则等待其结果。catch 是 then(undefined, onRejected);在 catch 中只打印而不重新抛出,会把下游转为 fulfilled(通常值为 undefined)。

finally 有不同契约:回调不接收结果,普通返回值不替换原成功值/失败原因;若抛错或返回拒绝的 Promise,才用新的失败覆盖原结果。不要把 finally(() => fallback) 当成错误恢复,也不要丢弃可能拒绝的 p.finally(cleanup) 返回链。

Promise.resolve(1).finally(() => 99).then(console.log); // 1
Promise.reject('失败').catch(() => '降级值').then(console.log); // 降级值
Promise.resolve(1).finally(() => { throw new Error('清理失败'); })
  .catch(error => console.log(error.message)); // 清理失败

组合 API 接受 iterable,元素也可以是普通值/thenable,结果位置遵循输入顺序。all([])、allSettled([]) 成功得到 [],race([]) 永远 pending;Promise.reject(thenable) 直接把对象作为拒绝原因,不吸收它。all 的首错或 race 的胜出都不会自动取消其他操作。更多组合示例复用 ES6 Promise API。

手写 Promise 的最小实现范围

面试中的“手写 Promise”通常不是要求完整复现规范,而是考察能否说明以下核心职责:

  1. 保存 pending、fulfilled、rejected 三种状态,以及对应的结果值或失败原因。
  2. 首次 resolve/reject 锁定结果;即使仍在等待 thenable,后续决议也无效。
  3. then 在 Promise 未完成时缓存成功与失败回调,完成后再异步依次执行。
  4. then 返回一个新的 Promise;返回值需要继续传递,返回 Promise 时需要等待它的状态。

可以先从状态、回调队列和链式返回值三部分拆开实现,再补齐异常捕获与 thenable 处理等 Promise/A+ 细节。

Promise.all的实际应用

// 并行加载多个资源
async function loadResources() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);
    return { users, posts, comments };
  } catch (error) {
    console.error('加载资源失败:', error);
  }
}

Promise.race的超时控制

下面仅让返回链超时拒绝,不会中止 fetch,也不会自动清理定时器。真实请求应结合 AbortSignal 并在结束时清理资源,见 取消、超时与错误边界。

function fetchWithTimeout(url, timeout = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

最佳实践

  1. 始终处理错误:使用catch捕获Promise链中的错误
  2. 避免Promise嵌套:使用链式调用代替嵌套
  3. 合理使用Promise.all:并行执行独立的异步操作,提高效率
  4. 注意错误处理:Promise.all中一个失败会导致全部失败,如需单独处理可使用Promise.allSettled
// 使用Promise.allSettled获取所有结果,无论成功失败
Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('成功:', result.value);
      } else {
        console.log('失败:', result.reason);
      }
    });
  });

面试要点

  1. 能够清晰解释Promise的三种状态及其转换(深入阅读:Promise 的三种状态)
  2. 理解Promise如何解决回调地狱问题(深入阅读:Promise 的链式调用)
  3. 掌握Promise.all和Promise.race的区别和使用场景(深入阅读:两者的区别)
  4. 了解async/await与Promise的关系(async函数返回Promise)(深入阅读:async 函数)
  5. 能够手写简单的Promise实现(深入阅读:手写 Promise 的最小实现范围)

来源与取舍

结合 JS异步编程.pdf 第 3–7 页补充单次决议、thenable、reaction 入队与链式传播。修正“微任务只由 resolve/reject 创建”“采用 thenable 的 return 值”和 finally 与 then 混用的表述;基础 API 示例继续在本篇和 ES6 专题维护。