状态机(State Machine)是什么?
核心思想: 一个事物在任何给定时刻只能处于有限个状态中的一个。它从一个状态转移到另一个状态的过程称为转换(Transition) 。而触发这个转换的,通常是某个事件(Event)或条件(Condition) 。
XMLHttpRequest 对象就是一个更复杂的状态机。
-
状态: readyState 的 0, 1, 2, 3, 4。它在任何时候都只能是这五个值之一。
-
事件: open() 方法的调用、send() 方法的调用、从服务器接收到响应头、响应体开始下载等。
-
转换:
- 调用 open()(事件)使状态从 0(UNSENT)转换到 1(OPENED)。
- 网络上接收到响应头(事件)使状态从 1(OPENED)转换到 2(HEADERS_RECEIVED)。
- ...以此类推。
总结:状态机就是一种数学模型,用来描述一个系统在它的生命周期中如何响应不同的事件,并从一个状态转换到另一个状态。 它让复杂系统的行为变得可预测、可管理。
事件驱动(Event-Driven)是什么?
核心思想: 程序的执行流程是由事件来决定的,而不是像传统脚本那样从上到下顺序执行。
想象一个餐厅服务员:
-
传统流程(非事件驱动) : 服务员按顺序问第一桌“要点餐吗?”,然后等他们点完。再去问第二桌“要点餐吗?”,再等他们点完... 如果第一桌的客人一直在聊天不点餐,服务员就只能干等着,整个餐厅的效率会非常低。
-
事件驱动流程: 服务员站在那里,处于“待命”状态。
- 事件发生: 第一桌客人招手(这是一个“点餐”事件)。
- 事件处理: 服务员走过去,执行“记录菜单”这个任务。
- 事件发生: 第三桌客人喊“买单”(这是一个“结账”事件)。
- 事件处理: 服务员在点完第一桌的菜后,去给第三桌执行“结账”任务。
- 事件发生: 厨房按铃(这是一个“上菜”事件)。
- 事件处理: 服务员去执行“端菜”任务。
服务员的工作流程不是预先设定好的,而是由客人的各种“事件”来驱动的。程序也是如此。
在 Web 开发中:
浏览器就是一个典型的事件驱动环境。它大部分时间都在“待命”,等待各种事件发生:
- 用户点击鼠标 (click 事件)
- 用户滚动页面 (scroll 事件)
- 网络请求的状态改变 (readystatechange 事件)
- 定时器到时 (setTimeout 的回调)
你作为开发者,工作就是为这些你关心的事件注册“监听器”或“处理器” (也就是回调函数)。当事件发生时,浏览器就会调用你准备好的函数来处理它。
总结:事件驱动是一种编程范式,其中程序的流程由外部事件(如用户操作、网络活动、定时器)来主导。程序的核心是一个“事件循环(Event Loop)”,它不断地等待事件,然后分派给对应的处理函数。
XMLHttpRequest 完整状态机转换流程
状态 0: UNSENT (未发送)
-
含义: 这是 XMLHttpRequest 对象的初始状态。对象已经被创建,但还没有调用 open() 方法进行初始化。
-
你可以做什么: 在这个状态下,你几乎什么也做不了,除了调用 open() 方法。
-
进入此状态:
- const xhr = new XMLHttpRequest(); // 创建实例后,xhr.readyState 就是 0。
转换 0 → 1: open()
- 事件: 开发者调用 xhr.open(method, url, [async], [user], [password]) 方法。
- 动作: 你告诉了 XMLHttpRequest 对象你要请求的目标地址、HTTP 方法等基本配置信息。请求还没有被发送,只是做好了准备。
状态 1: OPENED (已打开)
-
含义: open() 方法已经被成功调用。请求的所有参数都已准备就绪,随时可以发送。
-
你可以做什么:
- 设置请求头:xhr.setRequestHeader('Content-Type', 'application/json');
- 设置 responseType 等其他属性。
- 最重要的是,调用 xhr.send() 来真正发起请求。
-
onreadystatechange 会触发吗? : 是的,从状态 0 转换到 1 时,onreadystatechange 会被第一次触发(如果已经设置了监听器)。此时 xhr.readyState 的值是 1。
转换 1 → 2: send() 与服务器响应
-
事件:
- 开发者调用 xhr.send([body]) 方法。此时,请求被真正发送到网络上。
- 浏览器等待,直到它从服务器接收到了第一个字节的响应,也就是 HTTP 响应头 (HTTP Headers) 和 状态码 (Status Code) 。
-
动作: 浏览器已经和服务器建立了连接,并且服务器告诉了浏览器这次响应的基本信息(比如 200 OK, 404 Not Found,以及 Content-Type, Content-Length 等头信息)。但响应体(真正的数据)还没有开始下载。
状态 2: HEADERS_RECEIVED (已接收到响应头)
-
含义: send() 已被调用,并且客户端已经接收到来自服务器的完整响应头。
-
你可以做什么:
- 获取响应头信息:xhr.getResponseHeader('Content-Type');
- 获取所有响应头:xhr.getAllResponseHeaders();
- 获取 HTTP 状态码:xhr.status (例如 200)
- 获取状态文本:xhr.statusText (例如 "OK")
-
onreadystatechange 会触发吗? : 是的,这是第二次触发。此时 xhr.readyState 的值是 2。这是非常有用的一个阶段,你可以提前知道请求是否成功(通过 status),而无需等待整个响应体下载完毕。
转换 2 → 3: 开始下载响应体
- 事件: 浏览器开始接收响应体 (Response Body) 的数据。这个过程可能是瞬时的(对于小数据),也可能是持续一段时间的(对于大文件)。
- 动作: 数据包正在从服务器源源不断地传输到客户端。
状态 3: LOADING (正在加载)
-
含义: 响应体正在下载中。数据还没有完全接收完毕。
-
你可以做什么:
- xhr.responseText 属性此时已经可用,但它只包含已经下载的部分数据。
- 这个状态对于实现下载进度条至关重要。你可以在 onreadystatechange 事件处理器中检查 readyState 是否为 3,然后从 xhr.responseText.length 或通过监听 progress 事件来更新进度。
-
onreadystatechange 会触发吗? : 是的,这是第三次(也可能是多次)触发。对于较大的文件,readyState 停留在 3 的阶段时,onreadystatechange 可能会被触发多次。每次触发,responseText 的内容都会增加。
转换 3 → 4: 响应体下载完成
- 事件: 浏览器已经成功接收了全部的响应体数据,或者请求失败/中止。总之,整个网络请求操作结束了。
- 动作: 浏览器关闭了与服务器的连接(除非使用了 keep-alive)。
状态 4: DONE (完成)
-
含义: 请求操作已完成。这不代表请求成功,只代表整个生命周期走完了。可能是成功了(HTTP 200),也可能是失败了(HTTP 404, 500),或者是网络错误。
-
你可以做什么:
- 这是最关键的状态。你需要在这里检查 xhr.status 来判断请求是否真的成功。
- 如果成功(xhr.status >= 200 && xhr.status < 300),就可以安全地使用 xhr.responseText 或 xhr.response 来获取完整的数据。
- 处理各种错误情况。
-
onreadystatechange 会触发吗? : 是的,这是最后一次触发。此时 xhr.readyState 的值是 4。绝大部分的业务逻辑都写在这个 if (xhr.readyState === 4) 的代码块里。
总结图示
codeCode
+----------------+
| 0: UNSENT | <-- new XMLHttpRequest()
+----------------+
|
| xhr.open()
v
+----------------+
| 1: OPENED |
+----------------+
|
| xhr.send() + Server sends headers
v
+----------------+
| 2: HEADERS_RECEIVED |
+----------------+
|
| Browser starts downloading body
v
+----------------+
| 3: LOADING | <-- (May trigger onreadystatechange multiple times)
+----------------+
|
| Browser finishes downloading body /or/ Request fails
v
+----------------+
| 4: DONE | <-- The end of the lifecycle. Check xhr.status here.
+----------------+
通过这个完整的流程,你可以清晰地看到 XMLHttpRequest 是如何作为一个严谨的状态机工作的。它通过 readyState 这个属性,向外部暴露了其内部的每一个关键节点,而 onreadystatechange 就是那个忠实的“状态播报员”。开发者则通过在这个“播报员”函数里写 if/else 或 switch,来响应不同的状态,从而完成一次网络请求的全部逻辑。
这也反过来凸显了 fetch 基于 Promise 的设计是多么简洁:它直接隐藏了这些中间状态,只给你两个承诺——一个关于响应头(第一个 .then),一个关于响应体(第二个 .then),大大简化了开发者的心智负担。
XMLHttpRequest 上传状态机模型
这个模型描述的是 xhr.upload 对象从 xhr.send() 被调用开始,到数据完全发送到服务器为止的生命周期。
状态 0: UPLOAD_IDLE (上传空闲)
-
含义: xhr.upload 对象已存在,但上传过程尚未开始。这是 send() 被调用之前的默认状态。
-
你可以做什么: 在这个状态下,最重要的事情是注册事件监听器。例如:xhr.upload.addEventListener('progress', ...)。
-
进入此状态:
- 调用 xhr.open() 后,xhr.upload 就处于这个准备状态。
-
与 readyState 的关系: 对应 readyState 为 1 (OPENED)。
转换 0 → 1: send() 调用
- 事件: 开发者调用 xhr.send(data) 方法,并且 data 不为空。
- 动作: 浏览器接管数据,准备启动网络传输。这标志着上传状态机的正式启动。上传的第一个事件 loadstart 会被触发。
状态 1: UPLOAD_STARTED (上传开始)
-
含义: 浏览器已经开始将第一个数据包发送到网络。上传过程正式启动。
-
对应的 xhr.upload 事件: loadstart
-
你可以做什么:
- 显示上传界面(如进度条)。
- 禁用提交按钮,防止重复点击。
- 显示状态文本:“上传开始...”。
-
loadstart 事件会触发吗? : 是的,从状态 0 转换到 1 时,loadstart 事件会被触发一次。
-
与 readyState 的关系: 此时 readyState 仍然是 1 (OPENED)。
转换 1 → 2: 数据传输中
- 事件: 浏览器正在网络上持续发送数据块。
- 动作: 这个转换不是瞬时的,而是一个持续的过程。在这个过程中,progress 事件会被反复触发。
状态 2: UPLOADING (上传中)
-
含义: 数据正在从客户端流向服务器。这是上传生命周期中最主要的工作阶段。
-
对应的 xhr.upload 事件: progress
-
你可以做什么:
- 这是最重要的状态,用于更新上传进度。
- 在 progress 事件的回调中,通过 event.loaded 和 event.total 计算百分比并更新 UI。
-
progress 事件会触发吗? : 是的,只要上传在进行,progress 事件就会被周期性地、多次地触发。
-
与 readyState 的关系: 整个上传过程,readyState 几乎总是停留在 1 (OPENED) 。因为浏览器正忙于“说”(发送数据),还没开始“听”(接收响应)。
转换 2 → 3: 上传传输结束
- 事件: 最后一个字节的数据已经成功发送,或者上传因错误、中止、超时而中断。
- 动作: 客户端的数据发送任务完成。接下来会根据结束的原因触发一个终结事件(load, error, abort, timeout)。
状态 3: UPLOAD_FINISHED (上传传输完成)
-
含义: 客户端的数据传输部分已结束。这不代表服务器已成功处理文件,仅表示数据已成功送达(或发送失败)。
-
对应的 xhr.upload 事件: load (成功), error (失败), abort (中止), timeout (超时)。
-
你可以做什么:
- 如果由 load 事件进入此状态:将进度条设置为 100%,并提示“等待服务器处理...”。
- 如果由 error 或 abort 事件进入此状态:显示错误信息,将进度条标为红色。
-
终结事件会触发吗? : 是的,根据上传结果,load, error, abort, timeout 中的一个会被触发一次。
-
与 readyState 的关系: 这是关键的交接点。当上传状态机进入状态 3 后,readyState 状态机才准备开始它的下一阶段:从 1 转换到 2 (HEADERS_RECEIVED),因为它现在可以开始接收服务器的响应了。
转换 3 → 4: 清理收尾
- 事件: 无论上传是成功、失败还是中止,整个上传流程都需要一个最终的收尾信号。
- 动作: loadend 事件被触发,标志着 xhr.upload 这条流水线的彻底终结。
状态 4: UPLOAD_ENDED (上传流程结束)
-
含义: xhr.upload 的所有相关活动都已结束。
-
对应的 xhr.upload 事件: loadend
-
你可以做什么:
- 执行最终的清理工作,例如重新启用上传按钮,隐藏进度条等。这是处理通用逻辑的最佳位置。
-
loadend 事件会触发吗? : 是的,在 load, error, abort, timeout 之后,loadend 总是会被触发一次。
-
与 readyState 的关系: 同状态 3,这是 upload 流程的终点,也是 readyState 流程继续前进的起点。
总结图示与对比
| 上传阶段模型 | 核心触发事件 (xhr.upload) | 含义 | 并行的 readyState |
|---|---|---|---|
| 0: UPLOAD_IDLE | (无) | 准备就绪,等待 send() | 1 (OPENED) |
| 1: UPLOAD_STARTED | loadstart | 上传开始 | 1 (OPENED) |
| 2: UPLOADING | progress (多次) | 数据正在传输 | 1 (OPENED) |
| 3: UPLOAD_FINISHED | load / error / abort | 数据传输结束(成功或失败) | 1 -> 2 的转换点 |
| 4: UPLOAD_ENDED | loadend | 上传流程完全终结 | 1 -> 2 的转换点 |
- xhr.upload 负责监控**请求体(Request Body)**的发送过程。
- xhr 本身(通过 readyState 和 onreadystatechange)负责监控**响应体(Response Body)**的接收过程以及整个 HTTP 事务的状态。
- 上传过程(xhr.upload 的生命周期)通常在 readyState 到达 2 之前就已经完成了。
XMLHttpRequest (XHR) 的概念是:一个内建于浏览器的 JavaScript 对象,它充当了一个“后台信使”,允许你的网页在不刷新整个页面的情况下,与服务器进行数据交换。
让我们把这个定义拆解成几个关键点:
-
它是一个 JavaScript 对象:
- 这意味着你可以用代码来创建它 (new XMLHttpRequest()) 并控制它 (xhr.open(), xhr.send(), xhr.onload = ...)。它是一个完全由前端开发者掌控的工具。
-
它工作在“后台”:
- 这意味着当 XHR 去服务器“取东西”或“送东西”时,用户当前的页面是不会被冻结或打断的。用户仍然可以滚动页面、点击按钮、输入文字。这就是所谓的异步 (Asynchronous) 。它解决了我们之前讨论的“整个页面刷新”的痛点。
-
它的任务是“数据交换”:
- 这是最革命性的一点。XHR 的目标不是去服务器取回一个完整的、新的HTML页面,而是去获取纯粹的数据(通常是 JSON 格式),或者向服务器发送数据。它把“内容(数据)”和“表现(HTML/CSS)”彻底分离开来。
-
它实现了“局部更新”:
- 当“信使”把数据从服务器带回来后,JavaScript 就可以接管,利用 DOM API (如 document.getElementById().innerHTML = ...),只更新页面上需要变化的一小块区域。
class Timer {
constructor(name) {
this.name = name;
this.onReady = null;
}
start() {
setTimeout(() => {
if (this.onReady) {
this.onReady();
}
}, 4000);
}
}
const timer = new Timer("MyTimer");
timer.onReady = function () {
console.log(`${this.name} is ready!`);
};
timer.start();
open(method, url, async, user, password) 异步不会造成阻塞
-
async: 我们几乎总是用 true (异步)。但了解一下同步请求 (false) 的情况:
codeJavaScript
// !!!强烈不推荐,仅作了解!!! xhr.open('GET', '/data', false); xhr.send(); // JavaScript 会在这里卡住,直到请求完成 // 只有请求完成后,下面的代码才会执行 console.log(xhr.responseText);同步请求会冻结浏览器 UI,导致页面无响应,用户体验极差,因此应绝对避免。
-
user 和 password: 用于基本的 HTTP 身份验证,会自动编码并添加到 Authorization 请求头中。
codeJavaScript
xhr.open('GET', '/protected-resource', true, 'myUsername', 'myPassword');
send(body)
send() 的参数 body 的类型取决于你要发送的数据:
- GET/HEAD 请求: xhr.send() 或 xhr.send(null)。
- 发送字符串: xhr.send('Just a string')。
- 发送 JSON: xhr.send(JSON.stringify({ key: 'value' }))。
- 发送 FormData: xhr.send(formData) (用于表单提交或文件上传)。
- 发送 Blob/File: xhr.send(myBlob)。
- 发送 ArrayBuffer: xhr.send(myArrayBuffer) (用于二进制数据)。
二、 强大的响应处理 (Response Handling)
除了 responseText,XHR 提供了更智能的方式来处理不同类型的响应数据。
xhr.responseType
这是一个至关重要的属性,必须在 send() 之前设置。它告诉 XHR 你期望服务器返回什么类型的数据,XHR 会尝试自动为你解析。
-
"" (空字符串,默认): 响应被视为文本,可以通过 xhr.responseText 获取。
-
"text": 同上。
-
"json": 超级常用! XHR 会自动将返回的 JSON 字符串解析为 JavaScript 对象。你可以通过 xhr.response 直接访问这个对象。
codeJavaScript
xhr.responseType = 'json'; xhr.onload = function() { // xhr.response 就是一个 JS 对象了,不再需要 JSON.parse() const user = xhr.response; console.log(user.name); }; -
"document": XHR 会将响应解析为一个 HTML 或 XML 文档对象,可以通过 xhr.responseXML 或 xhr.response 访问。
-
"blob": 响应被视为一个二进制大对象 (Blob),通常用于处理图片、音频、视频文件。
-
"arraybuffer": 响应被视为一个 ArrayBuffer,用于处理通用的、定长的二进制数据。
xhr.response vs xhr.responseText vs xhr.responseXML
- xhr.response: 首选属性。它的数据类型取决于 responseType 的设置。如果是 'json',它就是对象;如果是 'blob',它就是 Blob 对象。
- xhr.responseText: 总是返回响应的字符串形式。如果 responseType 不是 "" 或 "text",访问它可能会抛出错误。
- xhr.responseXML: 只有当 responseType 是 "document" 且响应是有效的 XML/HTML 时才可用。
xhr.onload 是什么?
onload 是一个事件处理程序,它会在 XHR 请求成功完成时被触发。
这里的“成功完成”是一个关键概念,它具体指:
- 请求已经发送出去。
- 服务器已经返回了响应。
- 整个响应体(response body)已经完全下载到浏览器端。
这在技术上等同于 readyState 变为 4 (DONE) 的那一刻。
核心要点: onload 事件的触发,只代表浏览器和服务器之间的通信过程顺利完成了。它不保证 HTTP 状态码是成功的(例如 200 OK)。一个 404 Not Found 或 500 Internal Server Error 的响应,同样会触发 onload,因为服务器确实“成功地”返回了一个完整的错误页面响应。
因此,在 onload 回调函数内部,你必须检查 xhr.status 属性来判断请求的业务逻辑是否真的成功。
如何使用 xhr.onload?
这是一个标准的使用模式,清晰且高效:
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users/1');
xhr.responseType = 'json'; // 推荐设置,自动解析JSON
// 设置 onload 事件处理函数
xhr.onload = function() {
// 在这里,请求已经完成,可以访问 xhr.status 和 xhr.response
// 步骤1: 检查 HTTP 状态码
if (xhr.status >= 200 && xhr.status < 300) {
// 请求成功 (例如 200 OK, 201 Created)
console.log('Request successful!');
console.log('Response data:', xhr.response);
} else {
// 服务器返回了错误状态 (例如 404 Not Found, 500 Server Error)
console.error('Server returned an error.');
console.error('Status:', xhr.status);
console.error('Status Text:', xhr.statusText);
}
};
// 为了健壮性,还应该处理网络层面的错误
xhr.onerror = function() {
console.error('Network request failed. Check your connection or CORS policy.');
};
xhr.send();
三、 监控请求进度 (Progress Events) - XHR 的王牌功能
这是 XHR 相对于早期 Fetch API 的核心优势之一。你可以精细地监控数据传输的进度。
1. 监控下载进度
下载进度指的是从服务器接收数据的进度。
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/large-video.mp4');
xhr.responseType = 'blob';
// 监听下载进度
xhr.onprogress = function(event) {
// event.lengthComputable: 一个布尔值,表示总大小是否可知
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Downloaded ${percentComplete.toFixed(2)}%`);
// 更新进度条
// progressBar.value = percentComplete;
} else {
// 无法计算进度,因为服务器没有在响应头中发送 Content-Length
console.log(`Received ${event.loaded} bytes`);
}
};
xhr.onload = function() {
console.log('Download complete.');
};
xhr.send();
- event.loaded: 当前已传输的字节数。
- event.total: 响应的总字节数 (需要服务器在响应头中提供 Content-Length)。
2. 监控上传进度
上传进度监控更为常用,例如在上传大文件时给用户一个实时的反馈。
关键点: 进度事件监听器需要绑定在 xhr.upload 对象上,而不是 xhr 对象本身。
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload-endpoint');
const formData = new FormData();
const largeFile = document.getElementById('fileInput').files[0];
formData.append('myFile', largeFile);
// 监听上传进度
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Uploaded ${percentComplete.toFixed(2)}%`);
// uploadProgressBar.value = percentComplete;
}
};
// 监听上传完成
xhr.upload.onload = function() {
console.log('Upload process finished, waiting for server response...');
};
// 监听整个请求(包括上传和服务器响应)的最终完成
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Server confirmed: Upload successful!');
}
};
xhr.send(formData);
xhr.upload 对象也有一系列自己的事件,如 onloadstart, onprogress, onload, onerror, onabort。
🧩 一、event 是什么?
在监听进度时:
xhr.onprogress = function (event) {
// ...
};
这里的 event 就是一个 ProgressEvent 对象,
它由浏览器在数据传输过程中不断触发,告诉你目前传了多少数据。
📦 二、ProgressEvent 常见属性
| 属性 | 类型 | 说明 |
|---|---|---|
event.lengthComputable | boolean | 是否能计算总大小(有的响应没有 Content-Length) |
event.loaded | number | 已经下载(或上传)的字节数 |
event.total | number | 总字节数(如果可计算) |
event.type | string | 事件类型,例如 "progress", "load", "error" 等 |
event.target | XMLHttpRequest | 事件所属的 XHR 对象 |
🔍 三、属性详细说明
🧮 1. lengthComputable
表示服务器是否提供了 文件总大小信息。
- 如果是
true,说明可以用loaded / total计算百分比。 - 如果是
false,说明服务器没有返回Content-Length,无法确定进度。
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`下载进度:${percent.toFixed(2)}%`);
}
📊 2. loaded
表示已经 加载完成的字节数。
浏览器每次接收到一部分数据就会更新这个值。
📦 3. total
表示文件的 总字节数,前提是 lengthComputable 为 true。
🪄 举个完整例子:
const xhr = new XMLHttpRequest();
xhr.open("GET", "/bigfile.zip", true);
xhr.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Downloaded ${percentComplete.toFixed(2)}%`);
} else {
console.log("无法计算总大小");
}
};
xhr.onload = function () {
console.log("下载完成!");
};
xhr.send();
💡 四、补充:上传时的进度
上传文件时,可以监听 xhr.upload.onprogress:
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`Uploaded ${percent.toFixed(2)}%`);
}
};
四、 控制请求 (Controlling the Request)
1. 中止请求 (abort())
在请求发出后,如果不再需要,可以随时中止它。这对于实现搜索框的自动完成(输入新字符时取消上一个请求)等功能非常有用。
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/some-data');
xhr.send();
// 比如,用户在 1 秒后关闭了弹窗,不再需要这个数据了
setTimeout(() => {
xhr.abort();
console.log('Request aborted.');
}, 1000);
xhr.onabort = function() {
console.log('The onabort event was fired.');
};
调用 abort() 后,readyState 会变为 4 (DONE),status 会变为 0。
2. 设置超时 (timeout 和 ontimeout)
如果请求在指定时间内没有完成,可以自动中止它。
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/slow-api');
// 设置超时时间为 3 秒 (3000 毫秒)
xhr.timeout = 3000;
xhr.ontimeout = function() {
console.error('Request timed out!');
// 在这里可以给用户提示,或者发起重试
};
xhr.send();
五、 处理 HTTP 头 (Headers)
1. 设置请求头 (setRequestHeader())
我们已经用过它来设置 Content-Type。它可以被多次调用来设置多个头。
codeJavaScript
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 一个常见的标记,表明是 AJAX 请求
xhr.setRequestHeader('Accept', 'application/json');
注意: 有些头是浏览器管理的,你无法手动设置,如 Host, User-Agent 等。
2. 获取响应头
-
getResponseHeader(headerName): 获取指定的响应头的值。
codeJavaScript
const contentType = xhr.getResponseHeader('Content-Type'); console.log(contentType); // "application/json; charset=utf-8" -
getAllResponseHeaders(): 获取所有响应头,以一个字符串的形式返回,每行一个。
codeJavaScript
const allHeaders = xhr.getAllResponseHeaders(); console.log(allHeaders); /* content-type: application/json; charset=utf-8 date: Tue, 11 Jun 2024 10:00:00 GMT ... */
总结:XHR 的高级用法清单
| 功能分类 | 方法/属性/事件 | 用途 |
|---|---|---|
| 响应处理 | xhr.responseType | 预设期望的响应数据类型 (json, blob, arraybuffer 等)。 |
| xhr.response | 获取根据 responseType 解析后的响应体。 | |
| 进度监控 | xhr.onprogress | 监控下载进度。 |
| xhr.upload.onprogress | 监控上传进度。 | |
| 请求控制 | xhr.abort() | 手动中止一个正在进行的请求。 |
| xhr.timeout | 设置请求的超时时间 (毫秒)。 | |
| xhr.ontimeout | 注册超时事件的回调函数。 | |
| 头部处理 | xhr.setRequestHeader() | 设置自定义的请求头。 |
| xhr.getResponseHeader() | 获取单个响应头的值。 | |
| xhr.getAllResponseHeaders() | 获取所有响应头的字符串。 | |
| 跨域凭证 | xhr.withCredentials = true | 在跨域请求中发送 Cookies 等身份凭证。 |
| 回调函数 | 监听对象 | 代表的阶段 | 触发时机 |
|---|---|---|---|
xhr.upload.onload | 上传阶段(客户端 → 服务器) | 文件成功发送完毕 | 数据已经传给服务器了,但服务器还没回复 |
xhr.onload | 整个请求 | 服务器响应已完全接收 | 服务端处理完并返回结果(比如 JSON) |
| codeJavaScript |
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 请求成功
console.log(xhr.response); // 使用 response 属性更佳
} else {
// 请求完成,但服务器返回了错误状态
console.error('Server error:', xhr.status, xhr.statusText);
}
};
xhr.onerror = function() {
console.error('Network request failed');
};
5. 发送请求 (把信投进邮筒)
使用 send() 方法将请求发出去。
codeJavaScript
// 对于 GET 请求,通常没有请求体,所以传入 null 或不传参数
xhr.send();
// 对于 POST 请求,请求体作为参数传入
const jsonData = JSON.stringify({ name: 'Alice', age: 30 });
xhr.send(jsonData);
三、 各种实用用法详解
1. GET 请求获取 JSON 数据 (最常用)
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
// 关键:设置 responseType 为 'json',XHR 会自动解析
xhr.responseType = 'json';
xhr.onload = function() {
if (xhr.status === 200) {
// xhr.response 现在是一个 JavaScript 对象,无需 JSON.parse()
console.log(xhr.response);
// { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
}
};
xhr.send();
2. POST 请求发送 JSON 数据
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts');
// 必须设置 Content-Type,告诉服务器我们发送的是 JSON
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.responseType = 'json';
xhr.onload = function() {
if (xhr.status === 201) { // 201 Created 是 POST 成功的常见状态码
console.log('Data posted successfully:', xhr.response);
}
};
const newPost = {
title: 'foo',
body: 'bar',
userId: 1,
};
// 发送时,需要将 JS 对象字符串化
xhr.send(JSON.stringify(newPost));
3. 文件上传与进度监控
这是 XHR 相比于早期 Fetch API 的一个巨大优势:原生支持进度事件。
codeHtml
<input type="file" id="fileInput">
<progress id="progressBar" value="0" max="100"></progress>
codeJavaScript
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('myFile', file); // 'myFile' 是后端接收文件的字段名
const xhr = new XMLHttpRequest();
// 监听上传进度事件
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
progressBar.value = percentComplete;
console.log(`Uploaded ${percentComplete.toFixed(2)}%`);
}
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed.');
}
};
xhr.open('POST', '/upload-endpoint');
// 使用 FormData 时,浏览器会自动设置正确的 Content-Type (multipart/form-data),
// 所以不要手动设置 xhr.setRequestHeader('Content-Type', ...)。
xhr.send(formData);
});
4. 处理超时
codeJavaScript
const xhr = new XMLHttpRequest();
xhr.open('GET', '/slow-resource');
// 设置超时时间为 5000 毫秒 (5秒)
xhr.timeout = 5000;
xhr.ontimeout = function() {
console.error('The request timed out.');
};
xhr.send();
四、 XHR vs. Fetch API
| 特性 | XMLHttpRequest (XHR) | Fetch API |
|---|---|---|
| API 设计 | 回调函数风格 (onload, onerror),事件驱动。 | Promise 风格,使用 .then(), .catch(), async/await,更现代。 |
| 易用性 | API 较为复杂,配置分散在多个方法和属性上。 | API 更简洁、语义化,链式调用清晰。 |
| 请求/响应对象 | 请求和响应的所有信息都集中在 xhr 这一个对象上。 | 拥有独立的 Request 和 Response 对象,设计更模块化。 |
| 错误处理 | onerror 处理网络层错误,HTTP 状态码错误需在 onload 中判断 xhr.status。 | Promise 只在网络失败时 reject。对于 404/500 等 HTTP 错误,它会 resolve,需要手动检查 response.ok 或 response.status。这是一个常见陷阱! |
| 进度监控 | 原生支持 onprogress (上传) 和 xhr.onprogress (下载),非常方便。 | 原生不支持。实现进度需要借助 ReadableStream,相对复杂。 |
| 浏览器兼容性 | 完美兼容所有浏览器,包括 IE。 | 现代浏览器都支持,但在一些非常老的浏览器上需要 Polyfill。 |