从 XMLHttpRequest 到 Fetch API:前端异步请求的演进之路与本质剖析

70 阅读4分钟

“阻塞还是非阻塞?”——这是每个前端开发者必须面对的灵魂拷问。

在现代 Web 应用中,数据驱动已成常态,而异步网络请求正是连接前端与后端的核心桥梁。从古老的 XMLHttpRequest,到如今主流的 Fetch API 和 Promise,再到优雅的 async/await,前端异步通信经历了深刻的变革。

本文将带你: ✅ 深入理解异步的本质
✅ 系统对比 XHR、Fetch、Promise 的设计哲学与实现差异
✅ 掌握最佳实践与常见陷阱
✅ 构建健壮、可维护的请求层架构

无论你是刚入门的新手,还是想梳理知识体系的资深开发者,这篇文章都值得你完整阅读。


🧠 一、异步的本质:为什么我们需要它?

同步 vs 异步:一场效率革命

想象一下你在银行排队办理业务:

  • 同步模式:前面一个人没办完,你就只能干等着——整个流程被“阻塞”。
  • 异步模式:你点完餐就可以去工作,服务员叫号时再回来取餐——不耽误其他事。

在浏览器中,JavaScript 是单线程运行的。如果一个操作(如网络请求)长时间占用主线程,页面就会“卡死”,用户无法点击、滚动或输入。

📌 这就是我们为何必须使用异步编程——为了保持应用的响应性与用户体验。

阻塞请求有多可怕?

// ❌ 危险!假设存在同步请求函数
const data = syncFetch('https://api.example.com/data');
console.log(data); // 必须等待...
console.log('这行代码永远不会立刻执行!');

在这期间,整个 UI 被冻结,用户体验极差。

而异步方案则完全不同:

// ✅ 安全!异步请求不会阻塞主线程
asyncFetch('https://api.example.com/data', (data) => {
  console.log('数据已到达:', data);
});
console.log('这条日志会立即打印!'); // 用户可继续交互

✅ 结论:异步 = 不阻塞主线程 + 提升响应速度 + 更好的用户体验。


⚙️ 二、元老登场:XMLHttpRequest(XHR)

作为前端异步请求的“开山鼻祖”,XMLHttpRequest 自 1999 年诞生以来,支撑了整整一代 Web 应用的发展。

尽管现在已被更现代的技术取代,但了解它的原理,有助于我们理解底层机制。

1. XHR 的基本使用流程

const xhr = new XMLHttpRequest();

xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
    const data = JSON.parse(xhr.responseText);
    document.getElementById('members').innerHTML = data
      .map(item => `<li>${item.login}</li>`)
      .join('');
  } else if (xhr.readyState === 4) {
    console.error('请求失败:', xhr.status);
  }
};

xhr.send();

核心步骤解析:

步骤方法说明
创建实例new XMLHttpRequest()初始化请求对象
配置请求xhr.open(method, url, async)设置方法、URL 和是否异步
设置头setRequestHeader()如 POST 需设置 Content-Type
监听状态变化onreadystatechange响应状态改变时触发
发送请求send(body)可携带请求体

2. 关键属性详解

属性含义
readyState请求生命周期状态(0~4)
statusHTTP 状态码(如 200、404)
responseText返回的文本内容

🔹 readyState 的五种状态:

  • 0: UNSENT(未初始化)
  • 1: OPENED(已调用 open)
  • 2: HEADERS_RECEIVED(收到响应头)
  • 3: LOADING(正在接收响应体)
  • 4: DONE(请求完成)

⚠️ 注意:只有当 readyState === 4 且 status 在 2xx 范围内才算成功。

3. XHR 的优缺点总结

✅ 优点❌ 缺点
兼容性极好(支持 IE7+)API 设计陈旧,基于事件监听
支持上传进度、超时控制容易陷入“回调地狱”
可手动 abort() 中断请求错误处理分散,需重复判断 status
功能全面,细节可控默认不支持 Promise,难以配合 async/await

💡 适用场景:维护老项目、需要精细控制请求过程(如监控上传进度)。


🚀 三、现代标准:Fetch API —— 更简洁、更强大

随着 ES6 的普及,Fetch API 成为新一代网络请求的标准,其核心理念是:让异步请求像写同步代码一样自然。

1. 基本语法:链式调用 + Promise

fetch('https://api.github.com/orgs/lemoncode/members')
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json(); // 解析 JSON 数据
  })
  .then(data => {
    const list = document.getElementById('members');
    list.innerHTML = data.map(user => `<li>${user.login}</li>`).join('');
  })
  .catch(error => {
    console.error('请求出错:', error);
  });

参数说明:

fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'Alice' }),
  credentials: 'include' // 若需携带 cookie
})

2. Fetch 的关键特性

特性说明
返回 Promise天然支持 .then().catch()
分离网络错误与 HTTP 错误网络断开 → reject;404/500 → resolve 但 !ok
流式响应处理response.body 是 ReadableStream,适合大文件
支持 AbortController可主动取消请求
更语义化的 Response 方法.json(), .text(), .blob()

⚠️ 重要提醒

fetch 只在网络错误(如断网)时抛出异常,对于 404、500 等状态码并不会自动 reject!必须手动检查 response.ok

3. 结合 async/await 写法更优雅

async function fetchMembersAsync() {
  try {
    const response = await fetch('https://api.github.com/orgs/lemoncode/members');
    
    if (!response.ok) throw new Error(`状态码: ${response.status}`);

    const data = await response.json();
    
    const list = document.getElementById('members');
    list.innerHTML = data.map(user => `<li>${user.login}</li>`).join('');

  } catch (error) {
    console.error('加载失败:', error.message);
  }
}

✨ 使用 async/await 后,异步代码几乎和同步代码一样直观!

4. Fetch 的优缺点对比

✅ 优点❌ 缺点
基于 Promise,易于链式调用IE 不支持,需 Polyfill
API 简洁,配置集中默认不带 Cookie(需设 credentials: 'include'
与 async/await 完美融合HTTP 错误不会自动 reject
支持流式处理大型响应取消请求需搭配 AbortController
更符合现代 JS 编程范式对 FormData 等处理稍复杂

🎯 推荐指数:★★★★★
👉 新项目首选方案!


💡 四、异步基石:Promise 的深入理解

如果说 Fetch 是“工具”,那么 Promise 就是“思想” 。它是现代异步编程的根基。

1. Promise 是什么?

Promise 表示一个尚未完成但将来会结束的异步操作的结果容器。

它有三种状态:

  • pending → 初始状态
  • fulfilled → 成功完成
  • rejected → 执行失败

一旦状态变更,就不会再变。

2. 创建一个 Promise

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    Math.random() > 0.5 
      ? resolve('成功获取数据') 
      : reject(new Error('网络超时'));
  }, 1000);
});

myPromise
  .then(result => console.log('✅', result))
  .catch(err => console.error('❌', err.message))
  .finally(() => console.log('请求结束'));

3. Promise 解决了哪些痛点?

✅ 痛点一:回调地狱(Callback Hell)

// ❌ 回调嵌套,层层缩进,难以维护
getUser(userId, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      // ... 还可能更多嵌套
    });
  });
});
// ✅ 使用 Promise 扁平化处理
getUser(userId)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .then(comments => { /* ... */ })
  .catch(handleError);

✅ 痛点二:统一错误处理

以前每个回调都要单独处理错误,现在只需一个 .catch() 捕获所有异常。


4. 常用静态方法一览

方法描述使用场景
Promise.resolve(value)返回已成功的 Promise包装同步值为异步
Promise.reject(reason)返回已失败的 Promise主动抛出错误
Promise.all(promises)全部成功才成功,任一失败即失败并行请求全部成功才继续
Promise.race(promises)第一个完成的 Promise 决定结果实现超时控制
Promise.allSettled(promises)等待所有完成(不论成败)批量任务统计结果
Promise.any(promises)第一个成功的 Promise多源请求择优返回

示例:请求超时控制(race)

const fetchWithTimeout = (url, timeout = 5000) => {
  const controller = new AbortController();
  
  const fetchPromise = fetch(url, { signal: controller.signal });
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => controller.abort(), timeout)
  );

  return Promise.race([fetchPromise, timeoutPromise]);
};

🆚 五、终极对比:XHR vs Fetch vs Promise

特性XMLHttpRequestFetch APIPromise
类型浏览器原生对象浏览器新 APIES6 规范对象
异步模型事件驱动 (onreadystatechange)基于 Promise基于状态机
返回值undefined(通过事件获取结果)Promise<Response>Promise<T>
错误处理onerror + 手动判断 statuscatch(仅网络错误)+ 手动判断 response.okcatch 统一捕获 reject
是否支持 async/await❌ 不直接支持✅ 天然支持✅ 天然支持
是否易产生回调地狱✅ 容易❌ 可避免❌ 核心就是解决这个问题
浏览器兼容性✅ 极佳(IE7+)❌ 不支持 IE✅ 支持现代浏览器
推荐度⭐⭐☆(仅用于维护)⭐⭐⭐⭐⭐(推荐新项目)⭐⭐⭐⭐⭐(必备基础)

📌 一句话总结

  • XHR 是历史的见证者,适合维护旧系统;
  • Promise 是异步编程的思想基石;
  • Fetch 是当前最优选择,代表未来方向。

🛠️ 六、实战场景与最佳实践

1. 简单数据获取 → 直接用 Fetch + async/await

async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('用户不存在');
  return res.json();
}

2. 并行请求 → 使用 Promise.all

async function loadUserProfile(userId) {
  try {
    const [user, posts, profile] = await Promise.all([
      fetch(`/users/${userId}`).then(r => r.json()),
      fetch(`/posts?uid=${userId}`).then(r => r.json()),
      fetch(`/profile/${userId}`).then(r => r.json())
    ]);
    return { user, posts, profile };
  } catch (err) {
    console.error('加载失败:', err);
  }
}

3. 请求超时控制 → Promise.race + AbortController

function fetchWithTimeout(url, timeout = 8000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timeoutId));
}

4. 批量上传文件 → Promise.allSettled

const uploadTasks = files.map(file => uploadFile(file));

const results = await Promise.allSettled(uploadTasks);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`✅ 文件 ${index} 上传成功`);
  } else {
    console.warn(`❌ 文件 ${index} 失败:`, result.reason);
  }
});

5. 封装通用请求库(推荐做法)

// utils/request.js
const BASE_URL = 'https://api.example.com';

export default async function request(url, options = {}) {
  const config = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
      'Authorization': localStorage.getItem('token') // 自动注入 token
    }
  };

  try {
    const response = await fetch(BASE_URL + url, config);
    
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(errorData.message || `HTTP ${response.status}`);
    }

    return await response.json();

  } catch (error) {
    if (error.name === 'AbortError') throw error;
    console.error('[Request Error]', error.message);
    throw error;
  }
}

// 使用
const users = await request('/users', { method: 'GET' });

📝 七、总结:技术演进的背后是开发体验的升级

技术定位是否推荐
XMLHttpRequest异步请求奠基者❌ 仅限维护
Promise异步编程范式革新✅ 必须掌握
Fetch API现代浏览器标准✅ 新项目首选

🔁 演进路线图

XHR (事件驱动) 
→ Promise (链式调用) 
→ Fetch (基于 Promise) 
→ async/await (同步风格书写异步)

这场演进不仅仅是 API 的更新换代,更是 编程思维的进化——从“如何发起请求”转向“如何优雅地组织异步逻辑”。


🎯 最后建议

  1. 新项目一律使用 Fetch + async/await
  2. 封装统一请求函数,提升复用性与一致性
  3. 善用 AbortController 控制请求生命周期
  4. 对 HTTP 错误做统一拦截处理
  5. 必要时引入 Axios 等第三方库(尤其需要 IE 支持时)

📚 延伸学习推荐

  • MDN 文档:Using Fetch
  • 《You Don’t Know JS》系列 —— 深入理解 Promise
  • Axios vs Fetch 深度对比(适合企业级项目选型)

🎉 结语
从前端的“刀耕火种”时代,到如今声明式、函数式的异步编程,每一次技术迭代都在让我们离“写出更好代码”的理想更近一步。

希望这篇文章能帮你打通异步请求的知识脉络,构建更加稳健高效的前端架构!

💬 欢迎留言讨论:你还在用 XHR 吗?你的项目是如何封装请求的?


✅ 如果你觉得这篇文章有帮助,请点赞、收藏、分享给更多开发者朋友!