彻底吃透 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

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

经典例题

javascript 复制代码
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,支持异步与链式调用。

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

javascript 复制代码
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

javascript 复制代码
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 完全一致:

javascript 复制代码
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 链)。例如:

javascript 复制代码
// 源代码
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 传入非函数,则忽略该参数,原值直接传递下去。
javascript 复制代码
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 实现请求超时

javascript 复制代码
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,并在面试和实战中游刃有余。

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

相关推荐
bug总结5 分钟前
uniapp总结
开发语言·前端·javascript
晚安日记wanna12 分钟前
政务 Agent 面试真题五个得分点缺一就掉档
面试·架构
走到天涯海角17 分钟前
pinia和Vuex的区别以及使用pinia
开发语言·前端·javascript
晚安日记wanna23 分钟前
分布式和微服务差在哪从一次订单超时雪崩说起
面试·架构
晴天161 小时前
Web 页面内存溢出与 CPU 溢出:区别、诊断与最佳实践
前端·javascript
Highcharts.js1 小时前
常见报错排雷指南3:兼容性问题的官方解法
前端·javascript·react.js·可视化·highcharts·报错解法
言乐61 小时前
Python加速器2视频网页加速器
前端·javascript·css·python·音视频
ShineWinsu1 小时前
对于MySQL:索引的解析
linux·数据库·mysql·面试·笔试·索引·海量数据
志尊宝2 小时前
Vue3 零基础每日笔记(017):事件处理进阶——$event、多事件与事件修饰符全家桶
javascript·vue.js·笔记
光影少年2 小时前
react navite浏览器 Event Loop 和 Node Event Loop 的区别
前端·javascript·react native·react.js·前端框架