2.1 XMLHttpRequest 对象详解


2.1 XMLHttpRequest 对象详解

本节摘要:XMLHttpRequest(XHR)是 Ajax 的奠基性 API,把"页面脚本直接收发 HTTP 请求"变成了浏览器的一等能力。本节从一次"请求发出了但回调没执行"的排障现场出发,完整走一遍 XHR 的创建、配置、发送与响应接收,讲透 readyState 状态机、事件模型与 responseType 五种取值,并说明这个"老" API 在上传进度等场景里仍不可替代的原因。

问题现场:请求明明发出去了,回调却没进

一个真实的排障案例。新人写的代码大致是这样:创建 XHR、设置 onreadystatechangeopensend,页面上却毫无反应。打开网络面板,请求是绿的 200,响应体也在。也就是说——请求成功了,代码没跑

回头看代码,问题一行行排出来:判断成功用的条件是 xhr.readyState === 4 && xhr.status === 200,但服务器实际返回的是 201(创建成功)与 204(无内容)之间的某种非 200 状态码;更早的一版代码在 send 之后又调了一次 open,把前一个请求作废了;还有一版把 setRequestHeader 写在了 send 之后——头早就跟着请求飞走了,再设无效。

三行代码,三个知识点:状态机的语义、生命周期的顺序约束、头部设置的时机。这正是 XHR 的特点——API 简单,但生命周期与状态语义有严格契约,不理解契约就会写出"看似对但永不触发"的代码。本节的目标就是把这份契约完整讲清。

XHR 生命周期与状态机

XMLHttpRequest 生命周期状态图

XMLHttpRequest 生命周期状态图

一、创建与历史兼容

创建一个 XHR 对象只要一行:

const xhr = new XMLHttpRequest();

如果翻到十多年前的老代码,会看到一段"兼容写法":先判断 window.XMLHttpRequest 是否存在,不存在就退回 new ActiveXObject("Microsoft.XMLHTTP")——那是 IE6 及更早年代的遗迹,微软用 ActiveX 控件实现了最初的 XMLHTTP(1.2 节讲过这段历史)。今天任何还活跃的浏览器都有原生的 XMLHttpRequest,这段兼容判断只在你维护祖传代码时会遇到,删掉即可。

创建出来的对象此刻处于 readyState 0(未初始化):它存在,但还没绑定任何请求。XHR 是一次性的——一个实例对应一次请求的生命周期。虽然规范允许在某些条件下复用,但工程实践约定:一次请求一个新实例,复用是竞态与幽灵 bug 的温床。

二、配置与发送:open 与 send 的分工

open 负责声明"怎么发",send 负责真正发出。两者之间是插头部、挂事件的窗口期。

// open 的三个关键参数:方法 地址 是否异步 xhr.open('GET', '/api/users?page=2', true); xhr.send();

三个参数各有一点要展开。

方法决定语义:GET 取数据、POST 提交数据、PUT 整体替换、DELETE 删除。方法不是随便选的字母,REST 风格接口按它路由与校验,2.2 节会专门讲。

地址里,GET 请求的参数要自己拼在查询串上。拼接时务必编码:

// 搜索词可能包含 & = 中文等字符 不编码会破坏查询串结构 const keyword = 'a&b=1 你好'; const url = '/api/search?q=' + encodeURIComponent(keyword); // 结果 /api/search?q=a%26b%3D1%20%E4%BD%A0%E5%A5%BD

漏掉 encodeURIComponent 是新人高频 bug:搜索"a&b"时服务器只收到"a",因为 & 被当成了参数分隔符。

第三个参数 async,务必保持 true(异步)。设成 false 是同步模式:主线程原地阻塞到响应回来,页面冻结,控制台会直接警告,规范也已把它标记为废弃。它存在的意义只剩"某些必须串行的上古场景",现代代码里出现同步 XHR 几乎必然是事故。

带请求体的发送(典型是 POST JSON):

xhr.open('POST', '/api/articles/42/comments', true); // 头部必须在 open 之后 send 之前设置 xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify({ content: '写得不错' }));

这里有个顺序契约:setRequestHeader 必须在 open 之后、send 之前调用。开头的排障案例里"send 之后才设头"的写法,头根本不会随请求发出。另外 Content-Type 必须与实际请求体匹配——发 JSON 却声明表单格式,服务器解析直接失败,返回 400。

发送 FormData 时有个便利点:浏览器自动设置带边界符的表单 Content-Type,手动再设反而会破坏边界,导致服务端解析失败:

const form = new FormData(); form.append('title', '周报'); form.append('file', fileInput.files[0]); xhr.open('POST', '/api/upload'); xhr.send(form); // 正确 不设置 Content-Type

三、readyState 状态机:五个状态各自的含义

XHR 用 readyState 属性汇报生命周期进度,共五个值:

readyState 名称 含义 你需要关心吗
0 UNSENT 已创建,未调用 open 基本不用
1 OPENED open 已调用,可设头、可挂事件 只在调试时留意
2 HEADERS_RECEIVED 收到响应头,状态码可用 极少
3 LOADING 响应体正在到达(可能多次触发) 流式场景
4 DONE 响应完全接收或失败终结 唯一关心的状态

老式写法监听 onreadystatechange,每次状态变化都触发一次,所以函数体里必须先判断 readyState === 4 再干活,否则会在头到达、体到一半时各执行一次逻辑。开头的案例之一就是判断条件写漏了状态检查。

现代写法应该用 onloadonerror,语义直接、无中间状态干扰:

xhr.onload = function () { // 响应接收完成触发(不论状态码) if (xhr.status >= 200 && xhr.status < 300) { // 2xx 视为成功 注意 201 204 206 都在此列 别只写 === 200 handleData(xhr.response); } else { handleError('服务器返回 ' + xhr.status); } }; xhr.onerror = function () { // 网络层失败:断网 DNS 失败 被拦截 handleError('网络错误'); };

两者的分工要记牢:onload 处理"收到了响应"(包括 404、500),onerror 处理"根本没到 HTTP 层"的网络故障。把 404 当网络错误处理、或反过来漏掉 onerror 导致断网时回调永不触发,都是高频错误。此外还有 onabort(被 abort 取消)与 ontimeout(超时),第 4 章统一讲。

四、接收响应:responseType 决定拿到什么

XHR 默认把响应当文本给你(responseText),但 responseType 属性可以把接收器换成其他类型:

xhr.responseType = 'json'; // 必须在 send 之前设置 xhr.onload = function () { console.log(xhr.response); // 直接是解析好的对象 无需 JSON.parse };
responseType xhr.response 类型 典型场景
''(默认) 字符串 纯文本、HTML 片段
'text' 字符串 显式声明要文本
'json' 对象(浏览器自动解析) JSON 接口(推荐)
'blob' Blob 对象 图片、文件下载
'arraybuffer' ArrayBuffer 二进制流、音频、加密数据

两个实用细节。其一,responseType = 'json' 比自己 JSON.parse(xhr.responseText) 更稳:解析失败会走 onerror 路径,而不是抛一个没人接的异常。其二,取二进制响应时读 xhr.response不要responseText——对二进制响应该属性直接抛错,这也是"想把图片字节读成文本"这类尝试必然报错的根因。

一个接收图片并预览的完整例子:

function loadAvatar(url, imgEl) { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function () { if (xhr.status === 200) { imgEl.src = URL.createObjectURL(xhr.response); // Blob 转对象 URL } }; xhr.send(); }

五、同步陷阱、复用陷阱与事件监听时机

三个最容易踩的契约类坑,值得单独立一节讲清。

陷阱一:send 是异步的。xhr.send() 调用后立即返回,不代表响应到了。在 send 下一行直接读 xhr.response 拿到的必然是空——数据还在路上。所有对响应的处理都必须放进事件回调。这是从同步思维转到异步思维的第一道坎。

**陷阱二:一个实例一次请求。**对同一个实例连发两次 opensend,第二次会把第一次作废(状态重置)。循环里复用同一个 XHR 发多个请求,是"部分请求莫名消失"的经典原因。正确姿势是每次请求都 new 一个实例,让垃圾回收去操心清理。

**陷阱三:事件要在 send 前挂好。**虽然多数浏览器里 send 之后挂 onload 也能赶上(网络往返需要时间),但本地缓存命中时响应可能同步级别地快,晚挂的监听会错过事件。把"挂事件"放在"open 之后、send 之前",是无歧义的顺序。

六、XHR 依然不可替代的场景

Fetch 大行其道的今天,为什么还要花一整节学 XHR?因为至少三块阵地它仍是首选或唯一选择:

**上传进度监控。**XHR 的 upload.onprogress 事件能拿到已上传字节数,做进度条是原生能力;Fetch 早期完全没有上传进度,后来也只能靠流式请求体曲线实现,兼容性与便利性都不如 XHR。文件上传场景(4.3 节)至今大量使用 XHR 就是这个原因:

xhr.upload.onprogress = function (e) { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); progressBar.style.width = percent + '%'; } };

**老代码维护。**存量系统里 XHR 与基于它的 jQuery ajax 数量庞大,看不懂状态机就没法排查"回调不触发""请求被作废"这类问题——本节开头的排障案例每天都在发生。

**理解封装库的地基。**Axios 在浏览器端的底层就是 XHR(Node 端走 http 模块)。它暴露的 onUploadProgresstimeoutabort 配置项,参数直接映射到本节讲的 XHR 能力。懂了原生层,库的配置项就不再是黑盒咒语。

常见疑问解答

onreadystatechange 和 onload 能混用吗

能,但没必要。onload/onerror/ontimeout/onabort 是更现代的事件模型,各管一种终态;onreadystatechange 是老的轮询式回调。新代码统一用前者,可读性和正确率都更高。

status 为 0 是什么意思

通常意味着请求没有完成 HTTP 通信:跨域被拦截、被 abort、断网,或读了本地文件。看到 status 0 优先排查跨域与网络,而不是服务器逻辑。

GET 请求能带 body 吗

XHR 层面技术上可以发,但 HTTP 语义与多数服务器、缓存、代理都不支持,属于"能做但永远别做"。GET 的参数就放查询串,老老实实编码。

本节要点回顾

  • 顺序契约:创建 → open → 设头挂事件 → send → 回调处理,顺序错了就是"请求成功但代码没跑"。
  • readyState 只需关心 4:现代写法用 onload/onerror 替代 onreadystatechange,语义更直接。
  • onload 与 onerror 分工:onload 覆盖一切收到响应的情况(含 404/500,判断 2xx 要用范围而不是等于 200),onerror 专管网络层失败。
  • responseType 提前设:json 免解析、blob 与 arraybuffer 承接二进制;二进制响应别碰 responseText。
  • 三个陷阱:send 立即返回、实例不复用、事件先于 send 挂好。
  • XHR 未过时:上传进度、存量维护、Axios 地基,三块阵地仍靠它。

排障实战:把状态机用在真实故障上

把本节的知识点串成一次完整的排障演练。某天接到反馈:页面上的"保存草稿"功能时灵时不灵——点十次大概有两三次没反应。打开网络面板,发现在途请求都返回 200。按本节的契约逐项排查:

第一步查实例复用。看代码,保存函数用了模块级的一个 XHR 实例,每次保存都重新 open 加 send。用户连续快速点两次时,第二次 open 把第一次作废——恰好那一次的响应携带"保存成功"的确认,作废的请求不会触发任何回调。修复:函数内每次 new 一个实例,问题消失。这个案例印证了"一次请求一个实例"不是洁癖而是契约。

第二步查事件挂载时机。修完仍偶发失效,继续看:onload 是在 send 之后才赋值的。本地缓存命中时,响应可能在赋值前就绪(部分实现的同步派发),监听挂晚了就错过事件。把事件挂载挪到 send 之前,彻底解决。

第三步做回归验证。修复后写一个最小复现页:双击触发保存连发两次,断言两次都收到回调。这个三步走的过程,每一步用的都是本节讲过的契约条款——排障能力强弱,就看你把契约记得多牢

与 Fetch 的迁移对照

维护旧代码时经常要做"XHR 改 Fetch"的迁移,两张对照表心裡有数就不慌。事件模型迁移:onload/onerror 对应 then/catch(注意 Fetch 把 404 也算 then,要手动查 res.ok);responseType 迁移:json 对应 res.json()、blob 对应 res.blob();setRequestHeader 迁移到 headers 配置对象;超时与取消统一改用 AbortController(4.4 节);唯独 upload.onprogress 没有等价物——迁移带进度的上传时要留下 XHR 或换思路。迁移的原则:先补齐语义再换写法,丢掉任何一条错误分支的语义,都是把旧 bug 换成新 bug。

一个收尾的判断标准:什么时候可以说"XHR 学到位了"?给三个可检验的行为——拿到一段 XHR 代码,能在十秒内指出事件挂载顺序有没有问题;面对"请求成功但代码没跑"的故障,能不看资料列出四个排查方向;需要上传进度时,能不查文档写出 upload 的 progress 监听。三个行为都过关,XHR 对你来说就从"要背的 API"变成了"顺手的老工具"。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U