跳到主要内容
预计阅读 50 分钟

Javascript篇 | Promise

前言

Promise 是现代 JavaScript 异步编程的基石,也是面试中的超高频考点。面试官考 Promise,不只是看你会不会用 .then(),而是要考你:

  • 对 Promise 状态机制的理解是否透彻
  • 能不能手写一个简化版 Promise
  • 微任务队列的执行时序能不能分析清楚
  • Promise 的各种静态方法(all/race/allSettled/any)有什么区别

很多同学用了几年 async/await,但对 Promise 的底层机制一知半解。面试时一道 Promise 执行顺序题就能暴露问题。今天我们就从状态转换到手写实现,把 Promise 彻底讲透。

诊断自测

在开始之前,试着回答以下问题:

1. 以下代码的输出顺序是什么?

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');

2. Promise.allPromise.allSettled 的区别是什么?

3. 以下代码输出什么?

Promise.resolve(1)
  .then(val => {
    console.log(val);
    return val + 1;
  })
  .then(val => {
    console.log(val);
    throw new Error('oops');
  })
  .then(val => {
    console.log(val);
  })
  .catch(err => {
    console.log(err.message);
    return 'recovered';
  })
  .then(val => {
    console.log(val);
  });
点击查看答案

第1题: 输出顺序是 1, 4, 3, 2。同步代码先执行(1, 4),然后微任务(Promise.then → 3),最后宏任务(setTimeout → 2)。

第2题: Promise.all 在任何一个 Promise reject 时就会立即 reject;Promise.allSettled 会等所有 Promise 都完成(无论 resolve 还是 reject),返回每个 Promise 的结果对象。

第3题: 输出 1, 2, 'oops', 'recovered'。第三个 .then 被跳过(因为上一步 throw 了),catch 捕获错误后返回了正常值,后续的 then 正常执行。

如果三道题都答对了,说明你基础不错!继续阅读可以查漏补缺。如果有答错的,别担心,读完本文你就能彻底搞懂。

Promise 的三种状态

Promise 本质上是一个状态机,有且只有三种状态:

  • pending(等待中):初始状态,既不是 fulfilled 也不是 rejected
  • fulfilled(已完成):操作成功完成
  • rejected(已拒绝):操作失败

状态转换规则

这是面试必问的知识点:

  1. 初始状态是 pending
  2. pending → fulfilled:调用 resolve(value) 时触发
  3. pending → rejected:调用 reject(reason) 时触发
  4. 状态一旦改变就不可逆:fulfilled 和 rejected 都是终态
const promise = new Promise((resolve, reject) => {
  resolve('success');
  reject('fail');   // 无效!状态已经是 fulfilled 了
  resolve('again'); // 无效!状态已经是 fulfilled 了
});

promise.then(val => console.log(val)); // 'success'

💡 核心:Promise 的状态只能从 pending 变为 fulfilled 或 rejected,一旦变了就”冻结”,后续的 resolve/reject 调用全部无效。

then / catch / finally

then 的两个参数

then 接受两个可选参数:成功回调和失败回调。

promise.then(
  value => { /* fulfilled 时执行 */ },
  reason => { /* rejected 时执行 */ }
);

但在实际开发中,更推荐用 .catch() 处理 rejected:

promise
  .then(value => { /* 处理成功 */ })
  .catch(reason => { /* 处理失败 */ });

为什么? 因为 .then(onFulfilled, onRejected) 中的 onRejected 只能捕获上一个 Promise 的错误,而 .catch() 能捕获整条链上的任何错误。

promise
  .then(value => {
    throw new Error('then 里面抛出的错误');
  }, err => {
    // ❌ 这里捕获不到上面 then 中的错误
  });

promise
  .then(value => {
    throw new Error('then 里面抛出的错误');
  })
  .catch(err => {
    // ✅ 这里能捕获到
    console.log(err.message);
  });

链式调用的秘密

.then() 返回的是一个新的 Promise,这就是链式调用的基础:

Promise.resolve(1)
  .then(val => val * 2)  // 返回新 Promise,resolve(2)
  .then(val => val + 3)  // 返回新 Promise,resolve(5)
  .then(val => {
    console.log(val);     // 5
  });

返回值规则:

  1. 返回普通值 → 下一个 then 接收这个值
  2. 返回 Promise → 下一个 then 等待这个 Promise 完成
  3. 抛出错误 → 下一个 catch 接收错误
  4. 没有返回值 → 下一个 then 接收 undefined
Promise.resolve('start')
  .then(val => {
    console.log(val);         // 'start'
    return new Promise(resolve => {
      setTimeout(() => resolve('async result'), 1000);
    });
  })
  .then(val => {
    console.log(val);         // 'async result'(等待 1 秒后)
    // 没有 return
  })
  .then(val => {
    console.log(val);         // undefined
  });

finally

finally 不管 Promise 是 fulfilled 还是 rejected 都会执行,常用于清理操作:

fetchData()
  .then(data => processData(data))
  .catch(err => handleError(err))
  .finally(() => {
    hideLoading();  // 无论成功失败都要隐藏 loading
  });

finally 的回调不接受参数,它的返回值也不影响链的传递(除非抛出错误或返回 rejected Promise):

Promise.resolve('hello')
  .finally(() => {
    return 'world'; // 这个返回值会被忽略
  })
  .then(val => {
    console.log(val); // 'hello',不是 'world'
  });

Promise 的静态方法对比

这是面试的重点,必须烂熟于心。

Promise.all

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

规则:

  • 所有 Promise 都 fulfilled → 返回所有结果的数组
  • 任何一个 reject → 立即 reject,返回第一个 reject 的原因
const p1 = Promise.resolve(1);
const p2 = Promise.reject('fail');
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3]).catch(err => {
  console.log(err); // 'fail'
});

Promise.race

const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 1000));

Promise.race([slow, fast]).then(val => {
  console.log(val); // 'fast'
});

规则: 返回第一个完成(fulfilled 或 rejected)的 Promise 的结果。

Promise.allSettled

const p1 = Promise.resolve(1);
const p2 = Promise.reject('fail');
const p3 = Promise.resolve(3);

Promise.allSettled([p1, p2, p3]).then(results => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: 'fail' },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

规则: 等所有 Promise 都完成,无论成功失败。返回结果对象数组,每个对象有 status 字段。

Promise.any

const p1 = Promise.reject('err1');
const p2 = Promise.resolve('success');
const p3 = Promise.reject('err3');

Promise.any([p1, p2, p3]).then(val => {
  console.log(val); // 'success'
});

规则:

  • 返回第一个 fulfilled 的 Promise 的值
  • 如果所有都 reject,返回 AggregateError

四者对比表

方法成功条件失败条件返回值
all全部 fulfilled任一 rejected结果数组 / 第一个错误
race第一个完成第一个完成(如果是 reject)第一个结果
allSettled永远 fulfilled不会 reject结果对象数组
any任一 fulfilled全部 rejected第一个成功值 / AggregateError

💡 记忆口诀:all 要全过,race 看谁快,allSettled 都要等,any 有一个就行。

微任务队列与 Promise 的执行时序

这是面试中最容易出错的地方。

宏任务 vs 微任务

JavaScript 有两种异步任务队列:

  • 宏任务(macrotask):setTimeout、setInterval、I/O、UI 渲染
  • 微任务(microtask):Promise.then/catch/finally、MutationObserver、queueMicrotask

执行顺序: 每个宏任务执行完后,会清空所有微任务,然后再执行下一个宏任务。

console.log('script start');          // 1. 同步

setTimeout(() => {
  console.log('setTimeout');          // 5. 宏任务
}, 0);

Promise.resolve()
  .then(() => {
    console.log('promise1');          // 3. 微任务
  })
  .then(() => {
    console.log('promise2');          // 4. 微任务
  });

console.log('script end');           // 2. 同步

// 输出顺序:script start → script end → promise1 → promise2 → setTimeout

经典面试题:复杂的执行顺序

console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => {
    console.log('3');
  });
}, 0);

new Promise((resolve) => {
  console.log('4');  // Promise 构造函数中的代码是同步执行的!
  resolve();
}).then(() => {
  console.log('5');
});

setTimeout(() => {
  console.log('6');
}, 0);

console.log('7');
点击查看执行分析

第一轮(同步代码 / 主脚本):

  1. 输出 1
  2. 注册 setTimeout 回调(宏任务队列)
  3. 执行 Promise 构造函数,输出 4,resolve()
  4. .then 回调放入微任务队列
  5. 注册另一个 setTimeout 回调(宏任务队列)
  6. 输出 7

清空微任务队列: 7. 输出 5

第二轮(第一个 setTimeout): 8. 输出 2 9. .then 回调放入微任务队列

清空微任务队列: 10. 输出 3

第三轮(第二个 setTimeout): 11. 输出 6

最终输出: 1, 4, 7, 5, 2, 3, 6

手写简化版 Promise

面试中经常要求手写 Promise,我们来一步步实现核心功能。

第一步:基础结构

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
}

第二步:实现 then

then 是 Promise 的核心方法,需要处理三种情况:已完成、已拒绝、待定。

class MyPromise {
  // ... constructor 同上

  then(onFulfilled, onRejected) {
    // 参数穿透:如果不是函数,就用默认函数
    onFulfilled = typeof onFulfilled === 'function'
      ? onFulfilled
      : value => value;
    onRejected = typeof onRejected === 'function'
      ? onRejected
      : reason => { throw reason; };

    // then 必须返回新的 Promise
    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        // 用微任务模拟异步
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      };

      const handleRejected = () => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      };

      if (this.status === 'fulfilled') {
        handleFulfilled();
      } else if (this.status === 'rejected') {
        handleRejected();
      } else {
        // pending 状态,先存起来
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });

    return promise2;
  }
}

第三步:处理返回值

resolvePromise 处理 then 回调的返回值,是符合 Promise/A+ 规范的关键:

function resolvePromise(promise2, x, resolve, reject) {
  // 防止循环引用
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected'));
  }

  if (x instanceof MyPromise) {
    // 如果返回的是 Promise,等待其完成
    x.then(resolve, reject);
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // thenable 对象
    try {
      const then = x.then;
      if (typeof then === 'function') {
        let called = false;
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (err) {
      reject(err);
    }
  } else {
    // 普通值,直接 resolve
    resolve(x);
  }
}

第四步:添加静态方法

class MyPromise {
  // ... 以上代码

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let count = 0;
      const len = promises.length;

      if (len === 0) return resolve([]);

      promises.forEach((p, i) => {
        MyPromise.resolve(p).then(
          value => {
            results[i] = value;
            if (++count === len) resolve(results);
          },
          reject
        );
      });
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach(p => {
        MyPromise.resolve(p).then(resolve, reject);
      });
    });
  }
}

面试时不需要写得这么完整,重点是展示你理解 Promise 的核心机制:状态机 + 回调队列 + 链式调用 + 微任务。

async/await 与 Promise 的关系

async/await 是什么?

async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:

// Promise 写法
function fetchUser() {
  return fetch('/api/user')
    .then(res => res.json())
    .then(data => {
      console.log(data);
      return data;
    })
    .catch(err => console.error(err));
}

// async/await 写法
async function fetchUser() {
  try {
    const res = await fetch('/api/user');
    const data = await res.json();
    console.log(data);
    return data;
  } catch (err) {
    console.error(err);
  }
}

本质关系

  1. async 函数始终返回一个 Promise
  2. await 会暂停 async 函数的执行,等待 Promise 完成
  3. await 后面的代码相当于放在 .then()
async function foo() {
  console.log('1');
  const result = await Promise.resolve('2');
  console.log(result);
  console.log('3');
}

// 等价于:
function foo() {
  console.log('1');
  return Promise.resolve('2').then(result => {
    console.log(result);
    console.log('3');
  });
}

经典面试题:async/await 的执行顺序

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');
async1();
console.log('script end');
点击查看分析
script start
async1 start
async2
script end
async1 end
  1. 输出 script start
  2. 调用 async1(),输出 async1 start
  3. 执行 await async2(),调用 async2(),输出 async2
  4. await 暂停 async1async1 end 被放入微任务队列
  5. 回到主脚本,输出 script end
  6. 清空微任务队列,输出 async1 end

错误处理

async/await 的错误处理用 try/catch,比 Promise 链的 .catch() 更直观:

async function riskyOperation() {
  try {
    const data = await fetchSomething();
    const result = await processSomething(data);
    return result;
  } catch (err) {
    // 可以捕获上面任何一步的错误
    console.error('操作失败:', err);
    throw err; // 如果需要向上传播
  }
}

常见误区

误区1:Promise 构造函数中的代码是异步的

很多人以为 new Promise(fn) 中的 fn 是异步执行的。实际上它是同步的!

console.log('before');

new Promise((resolve) => {
  console.log('inside Promise'); // 同步执行!
  resolve();
});

console.log('after');

// 输出:before → inside Promise → after

只有 .then() / .catch() / .finally() 的回调才是微任务(异步)。

误区2:catch 会终止 Promise 链

catch 不会终止链,它会返回一个新的 fulfilled Promise(除非 catch 中又抛出错误):

Promise.reject('error')
  .catch(err => {
    console.log('caught:', err);
    return 'recovered';     // 返回正常值
  })
  .then(val => {
    console.log('then:', val); // 'then: recovered' ✅ 正常执行
  });

误区3:await 可以用在任何函数中

await 只能在 async 函数内部使用(或 ES Module 的顶层):

// ❌ 错误
function foo() {
  const result = await fetchData(); // SyntaxError
}

// ✅ 正确
async function foo() {
  const result = await fetchData();
}

// ✅ 顶层 await(ES Module)
const data = await fetch('/api/data');

误区4:Promise.all 中一个失败全部结果都丢失

有些同学认为 Promise.all 中一个 reject 了,其他 Promise 的结果就完全拿不到了。其实其他 Promise 仍然会执行完成,只是 Promise.all 返回的结果只包含第一个 reject 的原因。

如果需要所有结果(包括失败的),用 Promise.allSettled

const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/posts'),
  fetch('/api/comments')
]);

// 可以分别处理成功和失败的结果
const successes = results.filter(r => r.status === 'fulfilled');
const failures = results.filter(r => r.status === 'rejected');

小结

Promise 是 JavaScript 异步编程的核心,掌握了它就掌握了异步的半壁江山。

核心要点

  1. 三种状态:pending → fulfilled / rejected,不可逆
  2. then 返回新 Promise:这是链式调用的基础
  3. 微任务:Promise 回调是微任务,优先于宏任务执行
  4. 静态方法:all(全过)、race(谁快)、allSettled(都等)、any(有一个)
  5. async/await:Promise 的语法糖,await 后面的代码等价于 then 回调
  6. 手写核心:状态机 + 回调队列 + resolvePromise 处理返回值

本章思维导图

Promise
  • 三种状态
    • pending(初始)
    • fulfilled(成功)
    • rejected(失败)
    • 状态不可逆
  • 实例方法
    • then(链式调用核心)
    • catch(错误捕获)
    • finally(清理操作)
  • 静态方法
    • Promise.all(全部成功)
    • Promise.race(谁先完成)
    • Promise.allSettled(全部完成)
    • Promise.any(任一成功)
    • Promise.resolve
    • Promise.reject
  • 执行时序
    • 构造函数同步执行
    • then/catch/finally 是微任务
    • 微任务优先于宏任务
    • 每轮宏任务后清空微任务
  • 手写实现
    • 状态机(status/value/reason)
    • 回调队列(pending 时存储)
    • then 返回新 Promise
    • resolvePromise 处理返回值
  • async/await
    • async 返回 Promise
    • await 暂停执行
    • 等价于 then 回调
    • try/catch 错误处理

练习挑战

挑战一:基础(⭐)

以下代码的输出是什么?

const p = new Promise((resolve, reject) => {
  resolve('first');
  resolve('second');
  reject('error');
});

p.then(val => console.log(val));
p.catch(err => console.log(err));
答案与解析

输出:first

Promise 状态只能改变一次。第一次调用 resolve('first') 后状态变为 fulfilled,后续的 resolvereject 都无效。.catch 不会触发,因为 Promise 是 fulfilled 状态。

挑战二:进阶(⭐⭐)

写出以下代码的输出顺序:

Promise.resolve()
  .then(() => {
    console.log('A');
    return Promise.resolve('B');
  })
  .then(val => {
    console.log(val);
  });

Promise.resolve()
  .then(() => {
    console.log('C');
  })
  .then(() => {
    console.log('D');
  })
  .then(() => {
    console.log('E');
  });
答案与解析

输出顺序:A, C, D, B, E

关键点:当 .then 回调返回一个 Promise 时,需要额外的微任务来处理(V8 中通常需要两个额外的微任务 tick)。所以 B 的输出会被推迟。

详细分析:

  1. 两个 Promise 链的第一个 then 都入微任务队列
  2. 执行第一个:输出 A,返回 Promise.resolve('B')(需要额外 microtick)
  3. 执行第二个:输出 CD 入队
  4. 处理返回的 Promise(额外 microtick 1)
  5. 执行 DE 入队
  6. 处理返回的 Promise(额外 microtick 2),B 入队
  7. 输出 E…不,这里需要更精确的分析——实际上是 B 先于 E

注意:这道题的精确输出可能因 JavaScript 引擎版本不同略有差异。V8 最新版本的结果是 A, C, D, B, E

挑战三:综合(⭐⭐⭐)

实现一个 promiseTimeout 函数,如果传入的 Promise 在指定时间内没有 resolve,就自动 reject:

function promiseTimeout(promise, ms) {
  // 你的实现
}

// 使用示例
const slow = new Promise(resolve => setTimeout(() => resolve('done'), 5000));
promiseTimeout(slow, 2000)
  .then(val => console.log(val))
  .catch(err => console.log(err)); // 'Timeout after 2000ms'
答案与解析
function promiseTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Timeout after ${ms}ms`));
    }, ms);
  });

  return Promise.race([promise, timeout]);
}

核心思路:利用 Promise.race,把原始 Promise 和一个定时 reject 的 Promise 放在一起竞争。谁先完成就用谁的结果。如果原始 Promise 在超时前完成,就返回其结果;否则就被 timeout Promise reject。

进阶版——支持取消超时定时器(避免内存泄漏):

function promiseTimeout(promise, ms) {
  let timeoutId;
  const timeout = new Promise((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error(`Timeout after ${ms}ms`));
    }, ms);
  });

  return Promise.race([promise, timeout]).finally(() => {
    clearTimeout(timeoutId);
  });
}

自我检测

读完本章后,确认你能回答以下问题:

  • 能说出 Promise 三种状态及转换规则(单向不可逆)
  • 能解释 then 返回新 Promise 是链式调用的基础
  • 能区分 Promise.all / race / allSettled / any 的行为差异
  • 能解释为什么 Promise 构造函数中的代码是同步执行的
  • 能分析复杂的 Promise + setTimeout 混合执行顺序
  • 能手写简化版 Promise(至少包含 constructor + then + resolve + reject)
  • 能解释 async/await 与 Promise 的等价关系
  • 理解 catch 不会终止 Promise 链,它返回的是 fulfilled Promise
  • 能解释微任务和宏任务的执行顺序
  • 能在实际场景中选择合适的 Promise 静态方法

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90