Skip to content

Promise 异步编程面试题全解析

一、核心要点速览

💡 核心考点

  • 三种状态: Pending、Fulfilled、Rejected(状态不可逆)
  • 链式调用: then/catch/finally 返回新 Promise
  • 常用方法: all、race、allSettled、any
  • 错误处理: 捕获异常、错误传播

二、Promise 基础:状态机模型

1. 三种状态与转换规律

Promise 本质上是一个状态机,它解决了异步操作中“何时结束”以及“结果是什么”的标准化描述。

  • Pending (等待中): 初始状态,既没有成功,也没有失败。
  • Fulfilled (已成功): 操作成功完成,调用 resolve() 后进入此状态。
  • Rejected (已失败): 操作失败,调用 reject() 或抛出异常后进入此状态。

Promise 状态机模型

💡 状态特性

  1. 原子性:一旦状态从 Pending 改变,就永远固定,不能再次改变。
  2. 不可逆:Fulfilled 和 Rejected 之间不能互相转换。

2. 基本使用演示

javascript
const promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5
    success ? resolve('Data received') : reject('Network error')
  }, 1000)
})

promise
  .then(res => console.log('Success:', res))
  .catch(err => console.error('Error:', err))
  .finally(() => console.log('Operation finished'))

三、链式调用:告别回调地狱

1. 为什么能链式调用?

then 方法总是返回一个新的 Promise。这使得我们可以将多个异步操作串联起来,像写同步代码一样书写异步逻辑。

Promise 链式调用时序图

2. 返回值规则(面试高频)

then 的回调函数中:

  • 返回普通值:会被包装成 Promise.resolve(value)
  • 返回 Promisethen 返回的新 Promise 会“跟随”这个 Promise 的状态。
  • 抛出异常then 返回的新 Promise 会进入 Rejected 状态。
javascript
Promise.resolve(1)
  .then(x => x + 1)              // 返回 2,包装成 Promise
  .then(x => { throw x })        // 抛出异常
  .catch(err => err * 10)        // 捕获异常,返回 20
  .then(x => console.log(x))     // 输出 20

四、并发控制:静态方法全家桶

在处理多个并发请求时,合理选择静态方法可以极大地提升性能和用户体验。

Promise 静态方法核心逻辑对比

1. 核心方法详解

  • Promise.all([p1, p2...]):
    • 精髓全成则成,一败则败
    • 场景:页面初始化需要同时获取用户信息、菜单列表和配置项。
  • Promise.race([p1, p2...]):
    • 精髓快者为王
    • 场景:接口超时控制、多源响应竞争。
  • Promise.allSettled([p1, p2...]):
    • 精髓不离不弃
    • 场景:批量上传文件,无论单个成功失败,都要知道最终所有结果。
  • Promise.any([p1, p2...]):
    • 精髓一成即成
    • 场景:多 CDN 容灾切换,只要有一个节点响应即可。

五、深入:手写 Promise 实现机制

手写 Promise 是大厂面试的高频考点,它不仅考察你对 Promise 规范的理解,还考察了发布订阅模式闭包的应用。

1. 核心设计思路:发布订阅模式

当我们在代码中执行 new Promise 并在内部进行异步操作(如 setTimeout)时,.then() 会先于异步结果执行。此时 Promise 处于 pending 状态,我们需要把 .then 中传入的回调函数存起来,等异步操作完成后再统一取出执行

MyPromise 内部实现机制

2. 标准手写实现 (面试版)

这个版本包含了 Promise/A+ 规范的核心逻辑,并添加了详细注释:

javascript
/**
 * MyPromise 手写实现
 * 核心逻辑:状态机 + 发布订阅 + 异步处理
 */
class MyPromise {
  constructor(executor) {
    // 1. 初始化状态
    this.state = 'pending';
    this.value = undefined; // 成功的值
    this.reason = undefined; // 失败的原因

    // 2. 存储回调函数队列 (订阅)
    // 为什么是数组?因为同一个 promise 实例可以调用多次 .then()
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    // 3. 定义 resolve 函数
    const resolve = (value) => {
      // 只有 pending 状态可以转换 (状态不可逆)
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // 依次执行存储的回调 (发布)
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    // 4. 定义 reject 函数
    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 5. 立即执行执行器,捕获异常
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  // then 方法实现
  then(onFulfilled, onRejected) {
    // 参数校验 (透传功能)
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

    // 为了实现链式调用,必须返回一个新的 Promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 核心难点:处理不同状态下的逻辑
      
      // A. 如果状态已经是成功 (同步 resolve)
      if (this.state === 'fulfilled') {
        try {
          const x = onFulfilled(this.value);
          resolve(x); // 将回调结果传给 promise2
        } catch (e) {
          reject(e);
        }
      }

      // B. 如果状态已经是失败 (同步 reject)
      if (this.state === 'rejected') {
        try {
          const x = onRejected(this.reason);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      }

      // C. 如果状态还是 Pending (异步操作)
      if (this.state === 'pending') {
        // 核心:此时不能执行回调,而是把执行逻辑存入队列
        this.onResolvedCallbacks.push(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (e) {
            reject(e);
          }
        });

        this.onRejectedCallbacks.push(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (e) {
            reject(e);
          }
        });
      }
    });

    return promise2;
  }
}

// 补充静态方法:Promise.all
MyPromise.all = function(promises) {
  return new MyPromise((resolve, reject) => {
    let result = [];
    let count = 0;
    promises.forEach((p, index) => {
      // 确保是 Promise
      MyPromise.resolve(p).then(res => {
        result[index] = res;
        count++;
        if (count === promises.length) resolve(result);
      }, err => {
        reject(err); // 只要有一个失败,整体就失败
      });
    });
  });
};

// 补充静态方法:Promise.resolve
MyPromise.resolve = function(val) {
  return new MyPromise((resolve) => resolve(val));
};

3. 手写实现中的三个关键难点

  1. 为什么需要回调数组? 同一个 Promise 实例可以多次调用 .then()。如果不使用数组,后面的 .then 回调会覆盖前面的。
  2. 为什么 .then 要返回新 Promise? 这是实现链式调用的基础。原生的 Promise 规范规定,每个 .then 都会产生一个新的 Promise,而不是返回 this
  3. 什么是“透传”? 如果 .then() 没有传回调函数(例如 promise.then().then(res => ...)),我们需要保证结果能一直传到最后。代码中通过 onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val; 实现了这一点。

六、高频面试题

1. Promise 解决了什么问题?

回答:

  • 回调地狱 (Callback Hell):通过链式调用解决了多层嵌套导致的代码难以维护和调试的问题。
  • 信任问题:回调函数由第三方库调用时可能存在调用多次、不调用等问题,Promise 状态一旦改变即固定的特性保证了结果的可靠性。
  • 错误处理:通过 catch 实现了错误的冒泡捕获,比传统的回调错误处理更优雅。

2. Promise.all 中如果有一个失败了,其他请求会停止吗?

回答: 不会停止Promise.all 的“短路”机制只是意味着它会立即进入 Rejected 状态,但已经发出的异步操作(如 HTTP 请求)无法撤回,它们仍会继续执行直到完成,只是其结果会被 Promise.all 忽略。

3. 如何实现 Promise 的超时控制?

回答: 使用 Promise.race()。将业务请求的 Promise 与一个在指定时间后自动 Reject 的 setTimeout Promise 放入数组中,谁快就取谁的结果。

javascript
function timeoutWrapper(p, ms) {
  const timeout = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([p, timeout]);
}

4. .then() 的第二个参数和 .catch() 有什么区别?

回答:

  • .then(success, fail):第二个参数只能捕获当前 Promise 的错误,无法捕获 success 回调函数中抛出的错误。
  • .catch():可以捕获链条中之前任何位置抛出的错误,包括 then 中成功回调产生的错误。推荐使用 catch 进行统一处理。

七、面试答题模板:如何优雅地介绍 Promise?

“Promise 是 ES6 引入的异步编程解决方案,它本质上是一个容器,保存着某个未来才会结束的事件的结果。”

  1. 核心机制:它有三种状态(Pending、Fulfilled、Rejected),状态转换由 resolvereject 触发,且一旦改变即不可逆。
  2. 解决痛点:它通过链式调用解决了“回调地狱”,让异步代码拥有了同步的逻辑感;同时提供了 allrace 等方法方便处理并发。
  3. 实战经验:在项目中我常配合 async/await 使用,它是 Promise 的语法糖,让代码更简洁。在处理并发请求时,我会根据业务需求灵活选择 allSettled(如批量上传)或 any(如多源备份)。

八、记忆口诀

Promise 歌诀:

Promise 有三种态,
pending fulfilled rejected。
状态只能变一次,
链式调用解千愁。

then 接成功 catch 错,
finally 总是被执行。
all 要全都成功,
race 比谁快第一!

allSettled 等全部,
any 要一个成功。
错误传播向下走,
链尾记得加 catch!

九、推荐资源


十、总结一句话

  • Promise: 状态机 + 链式调用 = 异步编程标准化
  • all/race: 聚合并发 + 灵活控制 = 并发请求利器 🚀
  • 错误处理: catch 捕获 + 错误传播 = 健壮的异步代码
最近更新