🌟 手撕 Ajax:从原生 XHR 到 Promise 封装,再到 fetch 的演进之路

53 阅读4分钟

前言

Ajax 不是魔法,而是你掌控异步请求的第一把钥匙。

在现代 Web 开发中,页面无需刷新即可与服务器通信——这背后的核心技术就是 Ajax(Asynchronous JavaScript and XML) 。尽管名字里有 “XML”,但如今我们几乎都用 JSON 作为数据格式。本文将带你从最底层的 XMLHttpRequest 出发,一步步封装自己的 getJSON 方法,并最终理解现代 fetch API 的设计哲学。


一、Ajax 是什么?为什么需要它?

Ajax 允许网页在不重新加载整个页面的前提下,向服务器发送请求并动态更新部分内容。这种“局部刷新”极大提升了用户体验,也奠定了前后端分离架构的基础。

其核心技术依赖于浏览器提供的:

  • 原生 XMLHttpRequest(XHR)对象(传统方式)
  • 或现代 fetch API(基于 Promise)

整个过程是异步的,不会阻塞主线程,用户可继续操作页面。


二、原生 Ajax:繁琐但可控

使用 XMLHttpRequest 发起一个 GET 请求的基本流程如下:


// 1. 创建 XHR 对象
const xhr = new XMLHttpRequest();

// 2. 配置请求(方法、URL、是否异步)
xhr.open('GET', '/api/data', true);

// 3. 发送请求
xhr.send();

// 4. 监听状态变化
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    const data = JSON.parse(xhr.responseText); // 解析 JSON
    console.log(data);
  }
};

// 5. 捕获网络级错误
xhr.onerror = function () {
  console.error('请求失败:发生了网络错误(如断网、CORS 被阻止等)');
};

🔍 关键知识点解析

1. xhr.onreadystatechange

该事件监听 readyState 的变化。readyState 有 5 种状态:

状态名含义
0UNSENT尚未调用 open()
1OPENED已调用 open(),连接建立
2HEADERS_RECEIVED已收到响应头
3LOADING正在接收响应体(可能部分可用)
4DONE请求完成(无论成功或失败)

✅ 只有当 readyState === 4 时,才能安全读取响应内容。

2. xhr.onerror

仅在网络层面失败时触发,例如:

  • 断网
  • DNS 解析失败
  • 跨域请求被浏览器拦截(CORS)

⚠️ 重要提醒

  • HTTP 状态码如 404500 不会触发 onerror!它们属于“成功收到响应但业务失败”,需通过 xhr.status 判断。
  • onerror 不提供具体错误信息(出于安全限制)。

三、手撕 Ajax:用 Promise 封装 XHR

原生 XHR 冗长且容易出错。我们能否像调用函数一样使用 Ajax?

答案是:用 Promise 封装它!

✨ 目标:实现一个 getJSON(url) 函数

期望调用方式:

getJSON('/api/user')
  .then(data => console.log(data))
  .catch(err => console.error(err));

这正是 Promise 的典型应用场景!

📦 初版实现(存在缺陷)

function getJSON(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.send();

    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4 && xhr.status === 200) {
        const data = JSON.parse(xhr.responseText);
        resolve(data); // 返回解析后的 JS 对象
      }
    };

    xhr.onerror = function () {
      reject(console.log("请求失败")); // ❌ 错误写法!
    };
  });
}

⚠️ 问题分析

  1. HTTP 错误未处理:若返回 404,Promise 会“卡住”(既不 resolve 也不 reject)。
  2. reject 参数不当console.log() 返回 undefined,导致错误信息丢失。

✅ 优化后的健壮版本

javascript
编辑
const getJSON = (url) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.send();

    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            const data = JSON.parse(xhr.responseText);
            resolve(data); // 返回解析后的 JS 对象
          } catch (e) {
            reject(new Error('响应不是有效的 JSON'));
          }
        } else {
          reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
        }
      }
    };

    xhr.onerror = function () {
      reject(new Error('网络错误:请求无法完成(可能因跨域、断网等)'));
    };
  });
};

💡 现在,getJSON 真正做到了:传入 URL,返回 JSON 数据,自动处理各类错误


四、现代方案:fetch —— 浏览器内置的“手撕成果”

其实,我们刚刚封装的 getJSON,本质上就是 fetch 的简化版!

使用 fetch 发起请求

fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json(); // 自动解析 JSON
  })
  .then(data => console.log(data))
  .catch(error => console.error('请求失败:', error));

fetch 的优势:

  • 基于 Promise,天然支持链式调用
  • API 简洁,语义清晰
  • 支持现代特性(如 AbortController、流式响应等)

🔍 注意:fetch 不会因 HTTP 错误(如 404/500)自动 reject,需手动检查 response.ok

更优雅的写法:async/await

async function fetchData() {
  try {
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error('请求失败');
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error('Error:', err);
  }
}

五、Ajax 的核心价值与注意事项

✅ 优点

  • 无刷新交互:提升用户体验
  • 按需加载:减少带宽消耗,降低服务器压力
  • 前后端解耦:前端通过 API 与后端通信,架构更清晰

⚠️ 注意事项

问题解决方案
跨域(CORS)后端设置 Access-Control-Allow-Origin
SEO 不友好使用 SSR(服务端渲染)或预渲染(Prerender)
错误处理复杂统一使用 .catch(),区分网络错误与 HTTP 错误
兼容性fetch 不支持 IE;老项目可用 axios 或 polyfill

六、总结:Ajax 的演进脉络

阶段技术特点
原始时代XMLHttpRequest + 回调繁琐、易错、回调地狱
过渡时代手写 Promise 封装 XHR逻辑清晰、可复用
现代时代fetch / axios标准化、简洁、功能强大

从手撕 Ajax 到拥抱 fetch,我们不仅学会了封装,更理解了异步编程的本质:用确定的方式,处理不确定的结果。

掌握 Promise 和 Ajax 的底层原理,不仅能写出更健壮的代码,也能在面对 axiosumi-request 等高级封装库时,做到“知其然,更知其所以然”。