阅读视图

发现新文章,点击刷新页面。

彻底吃透 Promise:从状态、链式到手写实现,再到 async/await 底层原理

彻底吃透 Promise:从状态、链式到手写实现,再到 async/await 底层原理

面试必考,源码必问,日常必用 —— Promise 是 JavaScript 异步编程的基石。本文带你完整梳理 Promise 的核心知识,并深入 async/await 的底层实现。

一、为什么需要 Promise?

在 Promise 出现之前,我们靠回调函数处理异步。回调模式有三个致命问题:

  1. 回调地狱:异步任务层层嵌套,代码横向发展(金字塔结构),难以阅读和维护。
  2. 错误处理混乱:每个回调必须单独处理错误,容易遗漏;try/catch 无法捕获异步回调中的异常。
  3. 并发组合困难:并行执行多个任务并在全部完成后执行逻辑,需要手动计数器,极易出错。
  4. 信任问题(控制反转):将回调交给第三方库后,无法保证它会被正确调用(次数、时机、参数等)。

Promise 应运而生,它通过状态机 + 链式调用 + 统一错误处理 + 组合工具,彻底改变了异步编程的体验。


二、Promise 核心概念速览

2.1 三种状态

  • pending(进行中):初始状态。
  • fulfilled(已成功):调用 resolve 后到达此状态,并拥有一个最终 value
  • rejected(已失败):调用 reject 后到达此状态,并拥有一个最终 reason

重要规则:状态一旦定型(settled)就不可再变,且只能从 pending 转换为 fulfilledrejected

2.2 链式调用

thencatchfinally返回一个新 Promise,从而实现链式。

  • then(onFulfilled, onRejected):接收成功/失败回调。返回值决定新 Promise 的状态:

    • 返回普通值 → 新 Promise 用该值 resolve
    • 返回 Promise → 新 Promise 的状态与该 Promise 一致。
    • 抛出异常 → 新 Promise 用该错误 reject
    • 如果 onFulfilledonRejected 不是函数,会发生值穿透(原值直接传递)。
  • catch(onRejected):语法糖 then(undefined, onRejected)

  • finally(onFinally):无论成功失败都会执行,不接收参数,返回值被忽略(除非回调内抛出异常或返回 rejected Promise,则会中断链并传递新错误)。适合做清理工作。

2.3 静态方法一览

方法 行为 典型场景
Promise.all 全部成功才成功,任一失败则立即失败 多个接口数据都成功后才渲染页面
Promise.allSettled 等待所有定型,永不失败;返回结果状态数组 记录所有任务结果,即使部分失败
Promise.race 最快定型的 Promise 胜出(成功或失败) 设置超时计时
Promise.any 最快成功的 Promise 胜出;全部失败才失败 多个备用接口,取最快成功的响应
Promise.resolve 包装值为 resolved Promise 将 thenable 转换为真正 Promise
Promise.reject 包装值为 rejected Promise 快速返回失败

三、面试高频考点:事件循环与微任务

理解微任务(microtask)是写出正确 Promise 代码的前提。

  • 宏任务setTimeoutsetInterval、I/O、UI 渲染。
  • 微任务Promise.then/catch/finallyqueueMicrotaskMutationObserver

执行顺序:当前宏任务 → 所有微任务 → 下一个宏任务

经典例题

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1,4,3,2

解释:先执行同步代码(1,4),然后清空微任务队列(3),最后执行下一个宏任务(2)。


四、手写一个符合 Promise/A+ 规范的简化版 Promise

面试中常要求手写简易 Promise,核心包含:构造函数、thencatchresolvereject,支持异步与链式调用。

下面是一个符合规范的实现(重点注释):

class MyPromise {
  constructor(executor) {
    this.state = 'pending';   // 'fulfilled' | 'rejected'
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

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

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

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

  then(onFulfilled, onRejected) {
    // 值穿透处理
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

    const promise2 = new MyPromise((resolve, reject) => {
      const fulfilledMicrotask = () => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      };

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

      if (this.state === 'fulfilled') {
        fulfilledMicrotask();
      } else if (this.state === 'rejected') {
        rejectedMicrotask();
      } else if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(fulfilledMicrotask);
        this.onRejectedCallbacks.push(rejectedMicrotask);
      }
    });

    return promise2;
  }

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

// 辅助函数:处理 then 返回的 x(可能是普通值、Promise 或 thenable)
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected'));
  }
  if (x && (typeof x === 'object' || typeof x === 'function')) {
    let called = false;   // 防止多次调用 resolve/reject
    try {
      const then = x.then;
      if (typeof then === 'function') {
        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) {
      if (called) return;
      called = true;
      reject(err);
    }
  } else {
    resolve(x);
  }
}

关键点说明

  • 使用 queueMicrotask 模拟原生 Promise 的微任务行为。
  • 支持异步:当状态为 pending 时将回调存入队列,等待 resolve/reject 后执行。
  • 支持链式:then 返回新 Promise,并通过 resolvePromise 解包返回值。
  • 实现值穿透、错误冒泡、循环引用检测。

五、深入理解 async/await 的底层原理

async/await 是 ES2017 引入的语法糖,其底层基于 Promise + 生成器(Generator)

5.1 生成器 + Promise 模拟 async/await

生成器函数可以暂停(yield)和恢复(next),并且可以向外部传递值。利用这一点,我们可以编写一个执行器来自动驱动生成器,每次遇到 yield 就等待 Promise 完成,然后将结果传回生成器继续执行。

以下是一个简化版的执行器 run

function run(generatorFn) {
  const gen = generatorFn();

  function handle(result) {
    if (result.done) return Promise.resolve(result.value);
    return Promise.resolve(result.value).then(
      value => handle(gen.next(value)),
      error => handle(gen.throw(error))
    );
  }

  try {
    return handle(gen.next());
  } catch (err) {
    return Promise.reject(err);
  }
}

// 使用示例
function fetchData(url) {
  return new Promise(resolve => setTimeout(() => resolve(`数据来自 ${url}`), 1000));
}

const genAsync = function* () {
  const data1 = yield fetchData('https://api.example.com/user');
  console.log(data1);
  const data2 = yield fetchData('https://api.example.com/orders');
  console.log(data2);
  return '完成';
};

run(genAsync).then(console.log);

这段代码的行为与 async/await 完全一致:

async function asyncFunc() {
  const data1 = await fetchData('https://api.example.com/user');
  console.log(data1);
  const data2 = await fetchData('https://api.example.com/orders');
  console.log(data2);
  return '完成';
}
asyncFunc().then(console.log);

5.2 编译转换(Babel 视角)

当使用 Babel 将 async/await 编译到 ES5 时,会将其转换为生成器 + 执行器(或 Promise 链)。例如:

// 源代码
async function foo() {
  const a = await bar();
  return a;
}

// Babel 简化输出(类似)
function foo() {
  return _asyncToGenerator(function* () {
    const a = yield bar();
    return a;
  })();
}

其中 _asyncToGenerator 就是一个类似于上面 run 的执行器。

5.3 总结:async/await 的本质

层级 实现机制
最上层 async/await 语法(开发者编写)
转译/编译层 转换为生成器 + 执行器 或 Promise 链
执行层 生成器的 yield 暂停能力 + Promise 的异步通知
底层运行时 微任务(Microtask) + 事件循环

因此,理解 async/await 的关键在于掌握:

  1. Promise 提供了异步结果的标准表示和组合能力。
  2. 生成器 提供了函数执行的可暂停、可恢复能力。
  3. 执行器 将两者粘合,自动处理 Promise 的完成和拒绝,驱动生成器继续执行。

这也解释了为什么 async 函数总是返回 Promise,以及 await 只能出现在 async 函数中——因为生成器模式需要外部执行器驱动,而 async 函数正是这个执行器的容器。


六、高频面试题精选(附解答要点)

1. Promise 有哪几种状态?状态之间如何转换?

  • 三种:pendingfulfilledrejected
  • 转换:pending → fulfilled(调用 resolve),pending → rejected(调用 reject)。状态一旦定型不可逆。

2. then 方法返回的是什么?如何实现链式调用?

  • 返回一个新 Promise。新 Promise 的状态由回调的返回值决定。通过返回新 Promise 实现链式。

3. 什么是 Promise 的“值穿透”?举例。

  • 如果 then 传入非函数,则忽略该参数,原值直接传递下去。
Promise.resolve(42).then(null).then(v => console.log(v)); // 42

4. finally 能改变返回值吗?

  • 不能。返回值被忽略,原 Promise 的值或原因会继续传递。除非 finally 回调抛出异常或返回 rejected Promise,则会传递新错误。

5. Promise.allPromise.allSettled 的区别?

  • all:全部成功才成功,任一失败则立即失败(短路)。
  • allSettled:等待所有定型,总是成功,返回每个结果的状态对象数组。

6. 如何捕获 Promise 链中的错误?

  • 使用链尾的 .catch(),它会捕获链中任何地方抛出的错误(包括 then 回调中抛出的错误)。

7. 简述 Promise 的实现原理(手写简化版)。

  • 状态机 + 回调队列 + then 返回新 Promise + 微任务调度。详见上文实现。

8. 什么是微任务?为什么 Promise 的回调是微任务?

  • 微任务在当前宏任务执行完毕后、下一个宏任务之前执行。Promise 回调设为微任务是为了让异步结果尽快被处理,同时保持顺序可预测。

9. async/await 的底层实现是什么?

  • 基于 Promise 和生成器(Generator)的语法糖。通过执行器自动驱动生成器,每次 yield 一个 Promise,等待完成后恢复执行。

10. 如何将 Node.js 回调风格 API 转换为 Promise?

  • 使用 util.promisify 或手动 new Promise 包装。

七、实战:使用 Promise.race 实现请求超时

function fetchWithTimeout(url, timeoutMs = 5000) {
  const fetchPromise = fetch(url).then(res => res.json());
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('请求超时')), timeoutMs)
  );
  return Promise.race([fetchPromise, timeoutPromise]);
}

// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message));

注意Promise.race 不会取消未完成的请求,但可以控制超时后的行为。如果需要真正取消请求,可结合 AbortController


八、总结

Promise 的出现统一了 JavaScript 的异步模式,解决了回调地狱、错误处理和组合难的问题。掌握 Promise 是理解现代前端异步编程的基石,而 async/await 则是在 Promise 之上的优雅语法糖,其底层依赖生成器与执行器。希望本文能帮助你彻底吃透 Promise,并在面试和实战中游刃有余。

如果觉得有帮助,欢迎点赞、收藏、评论交流!

❌