JavaScript异步编程实例:Fetch API与async/await用法

作者:代码拾荒时间:2026-10-30 19:08:16浏览:553

深入讲解现代JavaScript中Fetch API与async/await的协作机制与工程实践。本文包含Fetch的两阶段Promise响应机制、async/await语法糖执行时序、HTTP状态码与网络异常分层处理、带超时控制的完整封装实例,以及并发性能优化策略。

很多初学者在第一次使用 fetch() 时,常常会遇到两个看似违反直觉的现象:把 fetch(url) 的返回值直接赋给变量并打印时,控制台输出的只是一个处于挂起状态的 Promise { <pending> };而在服务端返回 404 Not Found 或 500 内部服务错误时,绑定的 catch 代码块竟然完全没有被触发。理解这两个现象的核心,正是掌握现代 JavaScript 异步网络请求的关键。本文将通过剖析 Fetch API 的工作模型,并结合 async/await 语法糖,逐步演示从基础调用、两阶段响应解析、异常分层处理到带超时控制的工程级封装实战。

一、Fetch API 的工作模型:为何需要两阶段 Promise 决议?

传统的 XMLHttpRequest 依赖事件监听器回调,而现代浏览器原生的 Fetch API 完全基于标准 Promise 构建。在调用 fetch(url) 时,它并不会一次性把远端服务器的所有数据直接返回给调用方,而是采用流式传输(Streaming)的两阶段处理机制:

  1. 第一阶段(连接与响应头决议):当浏览器向服务器发起 HTTP 请求,只要服务器返回了响应头(HTTP Headers)并且建立好了连接,fetch() 返回的第一个 Promise 就会立即决议(Resolve)。此时我们拿到的是一个 Response 元数据对象。在这个阶段,响应体(Body)的二进制数据流可能才刚刚开始从网络套接字中传输,甚至还没有传输完毕。
  2. 第二阶段(响应体读取与解析):Response.body 是一个底层的可读流(ReadableStream)。要将流中的字节反序列化为可用的数据,我们需要调用 response.json()、response.text()、response.blob() 或 response.arrayBuffer()。这些方法本身也是异步操作,返回第二个 Promise,并在整个数据流完整读取且完成格式解析后才决议。
JavaScript异步编程实例:Fetch API与async/await用法
图 1:Fetch API 的两阶段流式传输与 Promise 决议机制

二、使用 async/await 简化异步流程

在 ES2017 之前,处理这两个阶段的 Promise 需要连续链式调用两次 .then()。如果遇到分支逻辑或多重嵌套,代码结构很容易失去重心:

// 传统的 Promise 链式写法
fetch('https://api.example.com/users/1')
  .then(response => {
    return response.json(); // 返回第二个 Promise
  })
  .then(data => {
    console.log('获取到的用户数据:', data);
  })
  .catch(error => {
    console.error('发生网络异常:', error);
  });

async/await 是对 Promise 调用的语法级抽象。它并没有推翻底层的事件循环与任务机制,而是允许我们用类似编写同步代码的顺序和心智模型来书写异步流程。标记为 async 的函数内部,遇到 await 操作符时会暂停该函数的进一步执行,让出 JavaScript 单线程的主调用栈,直到右侧的 Promise 完成决议,再以微任务(Microtask)的形式恢复执行并返回结果:

// 采用 async/await 改造后的写法
async function fetchUserData(userId) {
  // 1. 等待第一阶段:HTTP 响应头返回
  const response = await fetch(`https://api.example.com/users/${userId}`);

  // 2. 等待第二阶段:读取流并完成 JSON 解析
  const data = await response.json();

  return data;
}
JavaScript异步编程实例:Fetch API与async/await用法
图 2:async/await 与 JavaScript 单线程事件循环的任务调度协同时序

三、严谨的错误处理机制:为什么不能只靠 try/catch?

许多开发者在初次接触 async/await 时,习惯将全部代码包在一个 try...catch 块中,以为这样就万事大吉:

// 存在重大缺陷的典型写法
async function getBadExample() {
  try {
    const res = await fetch('/api/user/99999');
    const data = await res.json();
    return data;
  } catch (err) {
    // 警惕:当接口返回 404 或 500 时,这里根本不会执行!
    console.error('请求失败', err);
  }
}

必须铭记的核心规则:fetch() 仅在遇到真正的底层网络故障(如断网、DNS 解析失败、跨域 CORS 配置错误拒绝等)时,才会将返回的 Promise 置为 rejected 状态并触发 catch。对于 HTTP 协议层面的状态码(如 400 客户端参数错误、401 未认证、403 禁止访问、404 未找到资源、500 服务器内部错误),在网络层面上客户端都成功接收到了服务端给出的响应报文,因此 fetch() 会正常决议,并把状态反映在 response.ok 和 response.status 属性上。

其中,response.ok 是一个便捷的布尔属性,当且仅当 HTTP 状态码位于 200 到 299 的区间内时为 true,否则为 false。因此,健壮的网络请求逻辑必须在拿到响应后第一时间显式校验 response.ok,并在非成功状态下主动抛出业务或 HTTP 异常。

JavaScript异步编程实例:Fetch API与async/await用法
图 3:严谨的 Fetch 请求分支判断与错误分层处理流程

四、工程实战:带超时控制的完整封装

原生 fetch() 默认不会因为请求耗时过长而主动中断。如果在弱网或服务器挂起场景下,请求可能会悬挂数分钟,白白消耗资源并给用户带来糟糕的体验。在工程实践中,推荐结合现代浏览器标准提供的 AbortController 实现精确的超时中断控制。下面给出一个可在生产环境中直接参考使用的通用请求函数:

// 1. 定义专属的 HTTP 错误类,携带状态码与响应体
class HttpError extends Error {
  constructor(status, statusText, data) {
    super(`HTTP 错误 [${status}]: ${statusText}`);
    this.name = 'HttpError';
    this.status = status;
    this.statusText = statusText;
    this.data = data;
  }
}

// 2. 封装通用的请求工具函数
async function request(url, { timeoutMs = 8000, headers = {}, body, ...customOptions } = {}) {
  // 创建信号控制器
  const controller = new AbortController();
  const { signal } = controller;

  // 设置超时定时器
  const timeoutId = setTimeout(() => {
    controller.abort(new Error(`请求超时,超过了 ${timeoutMs}ms 限制`));
  }, timeoutMs);

  // 整理请求头与参数
  const config = {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      ...headers
    },
    signal,
    ...customOptions
  };

  if (body && typeof body === 'object') {
    config.body = JSON.stringify(body);
  }

  try {
    // 阶段一:等待连接与响应头
    const response = await fetch(url, config);

    // 阶段二:解析响应体内容(兼容 JSON 与文本)
    let payload;
    const contentType = response.headers.get('content-type') || '';
    if (contentType.includes('application/json')) {
      payload = await response.json();
    } else {
      payload = await response.text();
    }

    // 关键校验:检查 HTTP 状态码
    if (!response.ok) {
      throw new HttpError(response.status, response.statusText, payload);
    }

    return payload;
  } catch (error) {
    // 区分处理:判断是否因超时中止
    if (error.name === 'AbortError' || signal.aborted) {
      console.error('[网络超时]', error.message);
    } else if (error instanceof HttpError) {
      console.error(`[服务端报错 ${error.status}]`, error.data);
    } else {
      console.error('[底层网络异常/断网]', error.message);
    }
    // 向上层重新抛出,便于业务层做具体交互(如弹出 Toast)
    throw error;
  } finally {
    // 无论成功还是失败,均及时清除定时器,避免内存泄漏
    clearTimeout(timeoutId);
  }
}

使用时无论是 GET 还是 POST 请求,都能以极度平整的形式调用:

// 调用示例
async function submitUserForm(formData) {
  try {
    const result = await request('/api/users', {
      method: 'POST',
      timeoutMs: 5000,
      body: formData
    });
    console.log('提交成功,服务器响应:', result);
  } catch (error) {
    if (error instanceof HttpError) {
      alert(`提交失败:${error.data?.message || '服务器返回异常'}`);
    } else {
      alert('网络连接不稳定,请检查您的网络设置');
    }
  }
}

五、进阶场景:并发请求优化与 Promise 组合

当一个页面需要同时展示来自多个接口的数据(例如仪表盘需要同时加载“用户信息”、“订单列表”和“系统通知”)时,最容易出现的性能反模式就是无意识地写成连续串行等待:

// 性能较差的瀑布式等待写法(总耗时为三者相加:1s + 1.2s + 0.8s = 3.0s)
async function loadDashboardSequentially() {
  const user = await request('/api/user');
  const orders = await request('/api/orders');
  const notifications = await request('/api/notifications');
  return { user, orders, notifications };
}

由于这三个接口彼此之间没有参数依赖,正确的做法是利用 Promise.all 或 Promise.allSettled 触发并行网络 I/O,再用 await 一次性等待它们全部完成:

// 高性能并发模式(总耗时取决于最慢的一个请求:max(1s, 1.2s, 0.8s) = 1.2s)
async function loadDashboardConcurrently() {
  // 同时发起三个异步请求
  const [user, orders, notifications] = await Promise.all([
    request('/api/user'),
    request('/api/orders'),
    request('/api/notifications')
  ]);

  return { user, orders, notifications };
}

如果希望其中某一个接口的失败不影响其他模块的展示(例如通知接口偶尔故障不影响订单列表渲染),应使用 Promise.allSettled:

// 模块级容错并发
async function loadDashboardResilient() {
  const results = await Promise.allSettled([
    request('/api/user'),
    request('/api/orders'),
    request('/api/notifications')
  ]);

  const [userResult, ordersResult, notifyResult] = results;

  return {
    user: userResult.status === 'fulfilled' ? userResult.value : null,
    orders: ordersResult.status === 'fulfilled' ? ordersResult.value : [],
    notifications: notifyResult.status === 'fulfilled' ? notifyResult.value : []
  };
}
JavaScript异步编程实例:Fetch API与async/await用法
图 4:异步请求串行瀑布与并发优化模式效能对比

六、实践总结与速查清单

结合 Fetch API 与 async/await 构建现代前端网络交互时,建议在开发与 Code Review 中对照以下五条核心规范:

  • 牢记两阶段决议:fetch() 的结果是 Response 头信息与流句柄,解析内容需要第二次 await response.json()。
  • 绝不可漏查 response.ok:HTTP 4xx/5xx 不会触发 Promise reject,必须手动判断并抛出业务错误。
  • 流体只能消费一次:response.json() 或 response.text() 在同一个 Response 对象上只能读取一次,重复读取会抛出 TypeError: body stream already read;若需多处读取,应先调用 response.clone()。
  • 务必配置超时中止:借助 AbortController 与定时器限制挂起时长,防止网络死锁。
  • 避免无依赖的串行等待:多接口加载场景优先使用 Promise.all 或 Promise.allSettled 释放并发性能。
作者最新文章
相关文章
更多