文章目录

JavaScript异步编程:从Callback到Async/Await的演进

发布于 2026-04-02 03:15:14 · 浏览 13 次 · 评论 0 条

JavaScript异步编程:从Callback到Async/Await的演进

JavaScript 是单线程语言,但通过异步机制能高效处理网络请求、文件读写等耗时操作。若不掌握异步编程,程序会卡死或逻辑混乱。本文手把手带你理解三种主流异步写法,学会如何避免“回调地狱”,写出清晰可维护的代码。


1. Callback:最原始的异步方案

定义:Callback(回调函数)是一个作为参数传入另一个函数的函数,在异步操作完成后被调用。

使用场景:Node.js 的早期 API、浏览器中的 setTimeout 等。

编写一个模拟网络请求的函数

function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: "Alice" };
    callback(null, data); // 第一个参数为错误,第二个为数据
  }, 1000);
}

调用它并处理结果

  1. 定义 回调函数,接收错误和数据两个参数。
  2. 检查 err 是否存在:若存在,打印 错误;否则 使用 数据。
  3. 调用 fetchData 并传入该回调。
fetchData((err, data) => {
  if (err) {
    console.error("请求失败:", err);
  } else {
    console.log("用户信息:", data);
  }
});

问题暴露:当需要连续发起多个请求(如先获取用户ID,再用ID查订单),代码会层层嵌套:

fetchUser((err, user) => {
  if (!err) {
    fetchOrders(user.id, (err, orders) => {
      if (!err) {
        fetchProducts(orders[0].productId, (err, product) => {
          // 更深的嵌套...
        });
      }
    });
  }
});

这种结构被称为“回调地狱”——难以阅读、调试和修改。


2. Promise:解决嵌套问题的标准方案

定义:Promise 是一个代表异步操作最终完成或失败的对象。它有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。

改造上面的 fetchData 函数为 Promise 版本

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = { id: 1, name: "Alice" };
      // 成功时调用 resolve,失败时调用 reject
      resolve(data);
    }, 1000);
  });
}

使用 .then().catch() 处理结果

  1. 调用 fetchData(),它返回一个 Promise。
  2. 链式调用 .then() 处理成功结果。
  3. 链式调用 .catch() 捕获任意环节的错误。
fetchData()
  .then(data => {
    console.log("用户信息:", data);
    return data.id; // 返回值会传给下一个 .then
  })
  .then(userId => {
    console.log("用户ID:", userId);
  })
  .catch(err => {
    console.error("出错了:", err);
  });

关键优势

  • 链式调用避免了嵌套。
  • 错误统一在 .catch 中处理,无需每个步骤都检查 err

注意.then() 中抛出异常或返回 rejected Promise,都会跳转到最近的 .catch()


3. Async/Await:像写同步代码一样写异步

定义async/await 是基于 Promise 的语法糖,让异步代码看起来像同步代码。

规则

  • 在函数前加 async,该函数自动返回 Promise。
  • 在 Promise 前加 await,暂停执行直到 Promise 完成。

重写上述逻辑

  1. 声明 一个 async 函数。
  2. 在函数内部,用 await 调用 Promise 函数。
  3. 用 try/catch 包裹 可能出错的 await 调用。
async function getUserInfo() {
  try {
    const data = await fetchData();
    console.log("用户信息:", data);

    const userId = data.id;
    console.log("用户ID:", userId);
  } catch (err) {
    console.error("出错了:", err);
  }
}

// 调用 async 函数
getUserInfo();

处理多个依赖请求

async function fetchFullProfile() {
  const user = await fetchUser();       // 先获取用户
  const orders = await fetchOrders(user.id); // 再获取订单
  const product = await fetchProducts(orders[0].productId); // 最后获取商品
  return { user, orders, product };
}

并发请求优化:如果多个请求互不依赖,可用 Promise.all 并行执行:

async function fetchConcurrently() {
  const [user, config] = await Promise.all([
    fetchUser(),
    fetchAppConfig()
  ]);
  return { user, config };
}

这比串行执行快得多,因为两个请求同时发出。


4. 三种方式对比与选择建议

选择哪种方式取决于代码复杂度和团队规范。以下是核心差异:

特性 Callback Promise Async/Await
可读性 差(易嵌套) 中(链式) (线性)
错误处理 每层需手动检查 统一 .catch() try/catch
并发支持 难实现 Promise.all await Promise.all(...)
浏览器兼容 所有环境 ES6+ ES2017+

实际开发建议

  • 新项目一律使用 async/await:逻辑清晰,易于维护。
  • 老项目逐步迁移:将回调函数封装为 Promise,再用 await 调用。
  • 避免混合写法:不要在 async 函数里写 .then(),保持风格统一。

封装旧式回调为 Promise

// 假设 oldApi 是一个接受回调的函数
function promisify(oldApi) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      oldApi(...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

// 使用
const modernFetch = promisify(oldFetch);
const data = await modernFetch(url);

5. 常见陷阱与避坑指南

  1. 忘记 await

    async function badExample() {
      const promise = fetchData(); // 忘记 await
      console.log(promise); // 输出 Promise 对象,不是数据!
    }

    解决:确保所有异步调用前有 await

  2. 在循环中未并发处理

    // 错误:串行执行,很慢
    for (const id of ids) {
      const item = await fetchItem(id);
      process(item);
    }

    正确做法:先收集 Promise,再并发等待:

    const promises = ids.map(id => fetchItem(id));
    const items = await Promise.all(promises);
    items.forEach(process);
  3. 顶层 await 限制
    在普通脚本中不能直接使用 await(必须在 async 函数内)。
    解决:要么包裹在 async 函数中调用,要么使用模块(ESM)环境(支持顶层 await)。

  4. 错误未捕获导致静默失败

    async function risky() {
      throw new Error("Oops");
    }
    risky(); // 未处理 rejected Promise,可能无提示

    解决:始终用 .catch()try/catch 处理,或在顶层监听:

    window.addEventListener('unhandledrejection', event => {
      console.error('未捕获的异步错误:', event.reason);
    });

掌握 async/await 是现代 JavaScript 开发的必备技能。它让异步逻辑变得直观,大幅降低出错概率。从今天起,重构你的回调代码,拥抱更简洁的异步写法。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文