在 XMLHttpRequest 中,请求方式的选择、超时处理以及主动取消请求是三个紧密相关的主题。它们直接影响用户体验和代码健壮性。
同步请求:简单但阻塞主线程
将 XMLHttpRequest 第三个参数 async 显式设为 false 即可发起同步请求。同步请求会阻塞 JavaScript 执行,直到服务器响应返回,期间页面完全无法交互,表现为“假死”状态。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', false); // 第三个参数 false 表示同步
xhr.send();
// 代码会卡在这里,直到请求完成
console.log(xhr.responseText);
为什么不推荐同步请求?
它违背了异步非阻塞的设计初衷,严重影响用户体验。主线程被阻塞时,无法响应用户点击、滚动、甚至页面渲染也会停止。现代浏览器已经在主线程中废弃了同步 XHR,部分场景会直接抛出异常。仅在极少数特殊场景(如 Worker 线程中需要顺序获取资源)才可能使用,但仍需谨慎评估。
异步请求:默认且唯一的正确选择
不传第三个参数或显式传入 true,即为异步模式。请求发出后不会阻碍后续代码的执行,结果通过事件回调处理。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.responseText);
}
};
xhr.onerror = function() {
console.error('网络错误');
};
xhr.send();
console.log('请求已发出,继续执行');
异步模式下,代码逻辑需要围绕回调函数(或后续的 Promise 包装)来组织。这是 Web 应用的核心交互模型,确保用户界面始终响应。
超时:避免请求无限等待
XHR 提供了 timeout 属性和 ontimeout 事件,可以设定请求的最长等待时间。单位是毫秒,超过时间后自动终止请求并触发 ontimeout 回调。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/slow-endpoint');
xhr.timeout = 5000; // 5 秒超时
xhr.ontimeout = function() {
console.warn('请求超时,已取消');
// 可以展示用户友好的提示
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.onerror = function() {
console.error('网络错误或超时导致错误');
};
xhr.send();
注意:超时触发后,onerror 也可能被调用,需要适当区分。另外,timeout 只对异步请求生效,同步请求不支持设置超时。
中断:主动取消正在进行的请求
有时用户操作(如离开页面、切换组件)需要取消正在进行的请求,避免浪费带宽或造成状态混乱。使用 abort() 方法即可中断请求。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/large-data');
xhr.send();
// 某个条件触发取消
button.onclick = () => {
xhr.abort();
console.log('请求已手动取消');
};
xhr.onabort = function() {
console.warn('请求被取消');
};
abort() 调用后,readyState 会被重置为 0,并且触发 onabort 事件。在单页面应用中,配合组件销毁时(如 React 的 useEffect 清理函数或 Vue 的组件卸载钩子)取消未完成的请求是一个好习惯,可以防止对已卸载组件的状态更新。
实用组合:同时设定超时和手动中断
真实场景中,一个健壮的请求通常会同时使用超时和中断能力:
function createRequest(url, options = {}) {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url);
// 超时设置
if (options.timeout) {
xhr.timeout = options.timeout;
xhr.ontimeout = () => options.onTimeout?.();
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
options.onSuccess?.(xhr.responseText);
} else {
options.onError?.(new Error(`请求失败: ${xhr.status}`));
}
};
xhr.onerror = () => options.onError?.(new Error('网络错误'));
xhr.onabort = () => options.onAbort?.();
xhr.send();
// 返回取消函数,方便外部调用
return {
abort: () => xhr.abort()
};
}
// 使用
const request = createRequest('/api/data', {
timeout: 3000,
onSuccess: data => console.log(data),
onTimeout: () => alert('请求超时'),
onError: err => console.error(err),
onAbort: () => console.log('已取消')
});
// 5 秒后手动取消
setTimeout(() => request.abort(), 5000);
通过返回一个包含 abort() 方法的对象,可以将取消控制权交给调用方,实现类似 AbortController 的效果,也符合 Promise 化封装时的常见模式。这些基础能力是后续 Fetch API 和 Axios 等更高层封装的内置特性,但了解原生实现有助于理解它们的设计思想。