人人都会AI编程

16.1 XMLHttpRequest 原生实现

更新时间:2026-07-11

XMLHttpRequest(通常简称 XHR)是浏览器提供的原生网络请求接口。在 fetch 出现之前,几乎所有 Ajax 通信都基于它构建。即使今天前端开发普遍使用 fetch 或第三方库(如 Axios),理解 XHR 依然很有必要——它仍被大量遗留代码使用,并且可以帮助你更好地理解网络请求的底层流程。

创建 XHR 对象

使用 XMLHttpRequest 构造函数创建一个实例:

const xhr = new XMLHttpRequest();

注意,这是一个构造函数(首字母大写),每次调用都会产生一个新的请求对象。

发起请求

一次完整的 XHR 请求通常包含三个步骤:

1. 调用 open 初始化请求

xhr.open('GET', '/api/users', true);

open 方法接受三个参数:

  • method:HTTP 方法,如 'GET''POST''PUT' 等,大小写均可,通常用大写。
  • url:请求地址,可以是相对路径或绝对地址(需符合同源策略,跨域见第16.4节)。
  • async:布尔值,默认为 true,表示异步执行;显式传入 false 则为同步请求(在现代开发中极少使用,下文会说明原因)。

2. 设置请求头(可选)

在有需要自定义首部时,可以在 open 之后、send 之前调用 setRequestHeader

xhr.setRequestHeader('Content-Type', 'application/json');

该方法必须在 open 之后、send 之前调用。可以多次调用设置不同头部。

3. 调用 send 发送请求

xhr.send(null);  // GET 请求一般不带消息体,传 null 即可

对于 POST 等需要携带请求体的方法,send 接受字符串、FormData、Blob 等参数:

xhr.send(JSON.stringify({ name: 'Alice' }));

监听响应:readyState 与事件

XHR 通过 readyState 属性标识请求的当前状态,其值为一个整数(0~4):

| readyState | 状态说明 |
|------------|----------|
| 0 | UNSENTopen 还未调用 |
| 1 | OPENEDopen 已调用,但 send 未调用 |
| 2 | HEADERS_RECEIVED:已收到响应头 |
| 3 | LOADING:正在接收响应体(可能分块到达) |
| 4 | DONE:请求完成(无论成功还是失败) |

实际开发中,我们几乎只关心 readyState === 4 的状态。可以通过 onreadystatechange 事件监听状态变化:

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('成功', xhr.responseText);
    } else {
      console.error('请求失败', xhr.status);
    }
  }
};

现代代码中,更推荐使用 loaderrorabort 等专用事件(IE 10+ 支持),逻辑更清晰:

xhr.addEventListener('load', function () {
  if (xhr.status >= 200 && xhr.status < 300) {
    console.log('成功', xhr.responseText);
  } else {
    console.error('服务器返回错误状态码', xhr.status);
  }
});
xhr.addEventListener('error', function () {
  console.error('网络错误或请求被阻止');
});
xhr.addEventListener('abort', function () {
  console.warn('请求已被主动取消');
});

处理响应数据

XHR 提供了多种方式读取服务端返回的数据:

  • xhr.responseText:字符串形式的响应数据,无论是文本、HTML 还是 JSON,都可以先拿到字符串再手动解析(例如 JSON.parse(xhr.responseText))。
  • xhr.responseXML:如果服务端明确返回 Content-Type: text/xmlapplication/xml,浏览器会自动将响应解析为 XML 文档对象,否则该属性为 null
  • xhr.response:通用响应属性,可通过设置 xhr.responseType 决定其类型:
xhr.responseType = 'json';   // 自动将响应解析为 JS 对象
xhr.responseType = 'blob';   // 二进制大文件,如图片、视频
xhr.responseType = 'arraybuffer'; // 原始二进制缓冲,用于音频处理等

当设置了 responseType 后,对应的数据会放在 xhr.response 中,而 responseText 通常会变为空字符串。因此,在获取 JSON 数据时,可以直接让浏览器帮你解析。

同步请求:了解即可,永远别用

open 的第三个参数设为 false 会发起同步请求。此时 send 方法会阻塞主线程,直到请求完成才继续执行:

xhr.open('GET', '/api/data', false);
xhr.send(null);
// 代码会停在这里不动,直到响应返回
console.log(xhr.responseText);

这种写法有两个致命问题:

  1. 阻塞整个页面:请求期间页面完全假死,用户点击、滚动全部无响应,浏览器甚至会提示“脚本无响应”。
  2. 已被现代规范标记为废弃:主线程上禁止使用同步 XHR(Chromev80+ 已在主流上下文禁用)。

因此,无论出于用户体验还是未来兼容性,不要在生产代码中使用同步 XHR

设置超时

利用 timeout 属性可以指定请求的最大等待时间(毫秒)。当超时发生时,ontimeout 事件会被触发,并且 readyState 会变为 4:

xhr.timeout = 5000; // 5 秒
xhr.ontimeout = function () {
  console.error('请求超时,尝试重试或提示用户');
};

设置超时后,如果请求在指定时间内未完成,XHR 会自动将其终止。

取消请求

需要主动中止一个正在进行的请求时,调用 abort 方法:

xhr.abort();

这会触发 abort 事件(如果监听了),然后 readyState 变为 4,status 变为 0。常用于用户离开页面、输入框防抖时取消上一次请求。

完整示例

function ajax(url, { method = 'GET', data = null, headers = {}, timeout = 8000 } = {}) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open(method, url, true);
    xhr.timeout = timeout;

    // 设置请求头
    Object.keys(headers).forEach(key => {
      xhr.setRequestHeader(key, headers[key]);
    });

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        // 尝试自动解析 JSON
        try {
          resolve(JSON.parse(xhr.responseText));
        } catch {
          resolve(xhr.responseText);
        }
      } else {
        reject({ status: xhr.status, statusText: xhr.statusText });
      }
    };

    xhr.onerror = () => reject({ error: 'Network error' });
    xhr.ontimeout = () => reject({ error: 'Timeout' });
    xhr.onabort = () => reject({ error: 'Aborted' });

    xhr.send(data);
  });
}

// 使用
ajax('/api/user/123')
  .then(data => console.log('用户数据:', data))
  .catch(err => console.error('请求失败:', err));

小结与现状

XMLHttpRequest 是浏览器异步通信的功臣,但它也有一定局限性:

  • API 设计偏老旧,不支持 Promise,需要手动封装。
  • onreadystatechange 的事件模型繁琐。
  • 默认不携带 Cookie(需在 open 配合 withCredentials,且服务端需正确设置 CORS)。
  • 无法直接拦截请求和响应(相比 fetch 和 Axios 的拦截器)。

现代浏览器早已内置了更现代、更强大的 fetch API(第16.2节),以及在此基础上进行封装的三方库(如 Axios,第16.3节)。尽管如此,理解 XHR 仍能帮助你读懂老代码,掌握浏览器网络请求的底层运作方式,甚至在需要处理 progress 上传进度等 fetch 尚不原生支持的特性时,XHR 依然是可用的方案。