在日常开发中,我们几乎每天都在发 Ajax 请求,从后端取数据、更新页面。但你是否想过:JSON.stringify 的第二个参数有什么用?xhr.readyState 从 0 到 4 分别经历了什么?为什么 JS 明明是单线程,却能同时处理网络请求和界面点击?
这篇文章将带你从零开始,彻底搞懂 JSON 序列化 、Event Loop 事件循环 、XHR/fetch 请求过程,以及现代异步处理方案。全文配有完整可运行代码,你可以边看边试。
一、JSON 序列化与反序列化:数据跨网络传输的"翻译官"
当 JS 对象要通过网络发送给后端时,必须先"翻译"成字符串格式 ------ 这就是序列化 。后端返回的字符串也要变回 JS 对象 ------ 这就是反序列化。
1. JSON.stringify() 深度解析
bash
const todos = [
{ id: "1", title: "过四六级", completed: false },
{ id: "2", title: "回家过节", completed: false }
];
// 普通序列化
console.log(JSON.stringify(todos));
// 输出:[{"id":"1","title":"过四六级","completed":false},...]
完整语法 :JSON.stringify(value, replacer?, space?)
replacer:可以是一个数组(只保留指定属性)或函数(自定义处理)。传null表示全部保留。space:控制缩进空格数,提升可读性,便于调试。
javascript
// 只保留 title 和 completed,并且格式化缩进 2 格
console.log(JSON.stringify(todos, ['title', 'completed'], 2));
输出:
json
[
{
"title": "过四六级",
"completed": false
},
{
"title": "回家过节",
"completed": false
}
]
团队规范中,生产环境通常
space设为 0 以节省带宽,开发环境设为 2 便于阅读。
2. 反序列化:JSON.parse()
当后端返回 JSON 字符串时,必须转回 JS 对象才能操作:
ini
const responseText = '[{"id":"1","title":"过四六级"}]';
const todos = JSON.parse(responseText);
console.log(todos[0].title); // 过四六级
注意 :如果字符串格式错误(例如缺少引号),JSON.parse() 会抛出异常,建议用 try-catch 包裹。
二、JS 异步处理与 Event Loop:单线程如何"一心多用"?
JS 是单线程 语言,意味着一次只能做一件事。但为什么我们在页面上可以同时点击按钮、发起请求、定时器倒计时?这全靠 Event Loop(事件循环) 。
1. 同步与异步任务
- 同步任务 :立即执行,不等待(例如
console.log、普通函数调用)。 - 异步任务 :先"挂起",等到时机成熟(比如定时器到点、网络数据返回)再把回调函数扔进任务队列。
javascript
console.log('start'); // 同步,立即输出
setTimeout(() => {
console.log('timeout'); // 异步,4秒后回调进入任务队列
}, 4000);
console.log('end'); // 同步,立即输出
// 输出顺序:start → end → (4秒后) timeout
2. Event Loop 工作流程(简版)
- 执行调用栈中的所有同步代码。
- 当调用栈为空时,检查任务队列里有没有待执行的回调。
- 如果有,取出第一个回调放入调用栈执行。
- 重复第 2 步 ------ 这就是循环。
所以网络请求的回调不会阻塞页面渲染,因为请求本身是异步的,回调被扔进了任务队列等待主线程空闲。
三、两种主流异步处理方式:Promise 和 async/await
早期我们用回调函数(callback),但容易陷入"回调地狱"。后来 Promise 登场,接着是更优雅的 async/await。
1. Promise + then
javascript
fetch('http://localhost:3000/todos')
.then(res => res.json()) // 第一次异步:读取响应体
.then(data => {
console.log(data); // 拿到真正的数据
})
.catch(err => console.error(err));
2. async/await(推荐)
让异步代码看起来像同步,可读性最高:
javascript
async function loadTodos() {
try {
const res = await fetch('http://localhost:3000/todos');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
loadTodos();
注意:
await只能在async函数内部使用。
四、手写一个完整的 Ajax(XHR):请求过程全拆解
在 fetch 出现之前,XMLHttpRequest 是 Ajax 的真正核心。理解它的每个步骤,你就能彻底掌握 HTTP 请求的底层逻辑。
1. 完整前端代码(index.html)
xml
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<ul id="todos"></ul>
<button id="btn">点我请求数据</button>
<script>
document.getElementById('btn').addEventListener('click', () => {
const xhr = new XMLHttpRequest(); // 1. 创建实例
xhr.open('GET', 'http://localhost:3000/todos', true); // 2. 打开通道(true=异步)
xhr.onreadystatechange = function () { // 3. 监听状态变化
console.log('readyState:', xhr.readyState, 'status:', xhr.status);
if (xhr.status === 200 && xhr.readyState === 4) {
const todos = JSON.parse(xhr.responseText);
document.getElementById('todos').innerHTML =
todos.map(todo => `<li>${todo.title}</li>`).join('');
}
};
xhr.send(); // 4. 发送请求
});
</script>
</body>
</html>
2. 后端服务(server.js)
使用 Node.js 原生 http 模块提供 API,并开启 CORS 跨域支持。
ini
const http = require('http');
const todos = [
{ id: "1", title: "过四六级", completed: false },
{ id: "2", title: "回家过节", completed: false }
];
http.createServer((req, res) => {
// 允许任何域名访问(开发环境)
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (req.url === '/' || req.url === '/todos') {
res.end(JSON.stringify(todos));
} else {
res.statusCode = 404;
res.end('Not Found');
}
}).listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
启动命令:node server.js
3. XHR 请求过程的每一步详解
readyState 一共 5 个值,对应请求的生命周期:
| readyState | 状态名称 | 含义 | 此时你能做什么 |
|---|---|---|---|
| 0 | UNSENT | xhr 对象已创建,但 open() 尚未调用 |
仅可配置属性 |
| 1 | OPENED | open() 已被调用 |
可以设置请求头 |
| 2 | HEADERS_RECEIVED | send() 已执行,并且已收到响应头 |
可通过 getResponseHeader() 查看头信息 |
| 3 | LOADING | 正在接收响应体,responseText 已包含部分数据 |
可用于制作加载进度条 |
| 4 | DONE | 整个请求/响应过程结束 | 此时 responseText 完整,可安全处理数据 |
实际运行时的输出示例(点击按钮后控制台会依次打印):
lua
readyState: 1 status: 0
readyState: 2 status: 200
readyState: 3 status: 200
readyState: 4 status: 200
只有 readyState === 4 且 status === 200 时,才代表请求成功完成。
4. 可能出现的几种结果
- ✅ 成功 :status 200~299,readyState 走到 4,
responseText为合法 JSON。 - 🔄 重定向:status 301/302,需要手动处理或让 XHR 自动跟随(浏览器通常会自动跟随)。
- ❌ 客户端错误:status 404(未找到)、400(参数错误)等。
- 💥 服务端错误 :status 500,此时
readyState仍会变为 4,但业务上数据无效。 - ⏱ 网络超时/失败 :
onerror或ontimeout被触发,readyState可能永远达不到 4。
五、总结 & 最佳实践
| 知识点 | 关键要点 |
|---|---|
| JSON 序列化 | JSON.stringify(obj, null, 2) 格式化;JSON.parse(str) 注意异常捕获。 |
| Event Loop | 同步任务 → 调用栈;异步任务 → 任务队列;主线程空闲时从队列取回调执行。 |
| 异步进化史 | 回调函数 → Promise → async/await(最推荐)。 |
| XHR 请求步骤 | 1️⃣ 创建实例 → 2️⃣ open → 3️⃣ 监听 onreadystatechange → 4️⃣ send。 |
| readyState 含义 | 0(未初始化) → 1(已打开) → 2(收到头) → 3(加载中) → 4(完成)。 |
现在,当你再次写 Ajax 请求时,你不仅知道用 fetch 或 axios,还能清晰地理解底层每一步发生了什么。如果这篇文章帮到了你,欢迎点赞、收藏、评论三连,让更多人摆脱"只会用但不懂原理"的困境!