在异步的河流里,打捞被冻结的Ajax请求
🎭 场景重现:两段几乎一样的代码,命运却天差地别
我们来看两个 HTML 页面,它们都试图从 GitHub API 获取 lemoncode 组织的成员列表,并渲染到页面上。
❌ 版本 A(错误示范):
<body>
<ul id="members"></ul>
<script>
const xhr = new XMLHttpRequest();
console.log(xhr.readyState,'------');
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members',false); //false!
console.log(xhr.readyState,'|------');
xhr.send();
console.log(xhr.readyState,'|---|---');
console.log(xhr.responseText);
xhr.onreadystatechange = function() {
console.log(xhr.readyState,'|---|---|');
if(xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText);
const data = JSON.parse(xhr.responseText);
console.log(data);
document.getElementById('members').innerHTML = data.map(item => `<li>${item.login}</li>`).join('');
}
}
</script>
</body>
✅ 版本 B(正确写法):
<body>
<ul id="members"></ul>
<script>
const xhr = new XMLHttpRequest();
console.log(xhr.readyState,'------');
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members',true); //true !
console.log(xhr.readyState,'|------');
xhr.send();
console.log(xhr.readyState,'|---|---');
console.log(xhr.responseText);
xhr.onreadystatechange = function() {
console.log(xhr.readyState,'|---|---|');
if(xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText);
const data = JSON.parse(xhr.responseText);
console.log(data);
document.getElementById('members').innerHTML = data.map(item => `<li>${item.login}</li>`).join('');
}
}
</script>
</body>
表面看,只是 open() 的第三个参数不同(false vs true),但背后隐藏着前端开发中最经典的误区之一:误用同步请求。
今天,我们就以这两段代码为线索,揭开 Ajax 的神秘面纱,搞懂:
- 什么是同步/异步?
readyState到底怎么变?- 为什么“立刻读取 responseText”是危险操作?
- 如何写出健壮、高效、符合现代标准的 Ajax 请求?
🔍 一、核心差异全景对比表
| 对比维度 | ❌ 错误写法(同步) | ✅ 正确写法(异步) |
|---|---|---|
open() 第三个参数 | false | true(默认值) |
| 是否阻塞 JavaScript 主线程 | ✅ 是(页面卡死) | ❌ 否(非阻塞) |
responseText 可读时机 | 发送后立即可用(仅限同步) | 必须在 readyState === 4 时读取 |
| 事件监听器作用 | 几乎无效(请求已结束) | 核心逻辑所在 |
| 用户体验 | 极差(白屏/无响应) | 流畅(可加载动画等) |
| 浏览器支持 | 已被现代浏览器标记为 deprecated | 完全支持 |
| 适用场景 | 几乎不存在 | 所有真实项目 |
💡 划重点:Ajax 的“A” 就是 Asynchronous(异步) !用同步请求,等于自废武功!
🧪 二、深入 readyState:状态机的秘密
XMLHttpRequest 的 readyState 是一个状态机,共 5 个阶段:
| 值 | 状态名 | 含义 |
|---|---|---|
| 0 | UNSENT | 初始状态,new XMLHttpRequest() 后 |
| 1 | OPENED | 调用 open() 后 |
| 2 | HEADERS_RECEIVED | 调用 send(),且收到响应头 |
| 3 | LOADING | 正在接收响应体(可能未完成) |
| 4 | DONE | 请求完成,响应全部接收完毕 ✅ |
在你的控制台打印中观察变化:
✅ 异步版本(正确):
console.log(xhr.readyState, '------'); // 0
xhr.open(..., true);
console.log(xhr.readyState, '|------'); // 1
xhr.send();
console.log(xhr.readyState, '|---|---'); // 1(仍为1!因为异步,send 不阻塞)
// 后续由 onreadystatechange 触发:
// |---|---| → 2 → 3 → 4(逐步变化)
❌ 同步版本(错误):
console.log(xhr.readyState, '------'); // 0
xhr.open(..., false);
console.log(xhr.readyState, '|------'); // 1
xhr.send(); // ⚠️ 阻塞!直到请求完成
console.log(xhr.readyState, '|---|---'); // 4(直接跳到完成!)
// 但由于你在 xhr.send() 之后才绑定 onreadystatechange,
// 而同步请求在 send() 返回时早已完成(readyState=4),
// 因此监听器永远不会被触发——不是它不能触发,而是你“来晚了”。
💡 关键提醒:无论同步还是异步,事件监听器必须在 send() 之前绑定!否则在快速完成的请求中(尤其是同步),你会错过所有状态变化。
📌 关键洞察:
- 异步:
send()立即返回,后续靠事件驱动。- 同步:
send()会“卡住”直到服务器返回,期间整个页面无法交互!
💥 三、“立刻读取 responseText”为何是陷阱?
在错误代码中,有这样一行:
xhr.send();
console.log(xhr.responseText); // 危险!
问题在于:
- 在异步模式下:此时
readyState还是 1 或 2,responseText为空或不完整,读取无意义。 - 在同步模式下:虽然此时数据已就绪,但你绕过了状态检查,一旦网络出错(如 404、500),
responseText可能是错误信息而非 JSON,导致JSON.parse()崩溃!
✅ 正确做法:永远在 readyState === 4 且 status === 200 时处理数据!
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 成功处理
} else {
// 处理错误:404、500、超时等
console.error('请求失败:', xhr.status);
}
}
};
🚫 四、为什么同步 Ajax 是“前端毒药”?
1. 用户体验灾难
- 页面完全冻结,用户无法点击、滚动、输入。
- 移动端尤其明显:感觉“App 卡死了”。
2. 已被浏览器废弃
- Chrome、Firefox 等已对主线程中的同步 XHR 发出警告。
- MDN 明确指出: “不应在主线程中使用同步请求” 。
3. 无法处理复杂场景
- 不能设置超时(
timeout属性对同步无效) - 不能取消请求(虽然
abort()可用,但同步时已无意义) - 无法监听上传/下载进度
🆕 五、现代替代方案:拥抱 fetch 与 async/await
虽然理解 XMLHttpRequest 很重要,但实际开发中,请优先使用更现代的 API:
✨ 使用 fetch(原生支持):
fetch('https://api.github.com/orgs/lemoncode/members')
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(members => {
document.getElementById('members').innerHTML =
members.map(m => `<li>${m.login}</li>`).join('');
})
.catch(err => console.error('请求失败:', err));
✨ 使用 async/await(更优雅):
async function loadMembers() {
try {
const res = await fetch('https://api.github.com/orgs/lemoncode/members');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const members = await res.json();
document.getElementById('members').innerHTML =
members.map(m => `<li>${m.login}</li>`).join('');
} catch (err) {
console.error('加载失败:', err);
}
}
loadMembers();
💡 建议:学习时理解
xhr,开发时用fetch或axios!
🧠 六、补充:那些你可能忽略的细节
1. CORS 跨域问题
GitHub API 允许跨域,但很多私有 API 不允许。若遇到:
Blocked by CORS policy
说明后端未配置 Access-Control-Allow-Origin,需联系后端同学。
2. GitHub API 速率限制
未认证请求每小时最多 60 次。调试时注意不要频繁刷新!
3. 内存泄漏风险
长期存在的 xhr 对象若未正确释放(尤其在 SPA 中),可能导致内存泄漏。现代框架(React/Vue)通常自动处理,但原生开发需注意。