从 Ajax 到异步编程:JSON 序列化、Event Loop 与 XHR 请求完全解析

在日常开发中,我们几乎每天都在发 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 工作流程(简版)

  1. 执行调用栈中的所有同步代码。
  2. 当调用栈为空时,检查任务队列里有没有待执行的回调。
  3. 如果有,取出第一个回调放入调用栈执行。
  4. 重复第 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 === 4status === 200 时,才代表请求成功完成。

4. 可能出现的几种结果

  • 成功 :status 200~299,readyState 走到 4,responseText 为合法 JSON。
  • 🔄 重定向:status 301/302,需要手动处理或让 XHR 自动跟随(浏览器通常会自动跟随)。
  • 客户端错误:status 404(未找到)、400(参数错误)等。
  • 💥 服务端错误 :status 500,此时 readyState 仍会变为 4,但业务上数据无效。
  • 网络超时/失败onerrorontimeout 被触发,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 请求时,你不仅知道用 fetchaxios,还能清晰地理解底层每一步发生了什么。如果这篇文章帮到了你,欢迎点赞、收藏、评论三连,让更多人摆脱"只会用但不懂原理"的困境!

相关推荐
丷丩3 小时前
MapLibre GL JS第47课:添加动画图标
javascript·gis·动画·mapbox·maplibre
快乐的哈士奇3 小时前
【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
开发语言·javascript·ecmascript
云水一下3 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
kmblack14 小时前
javascript计算年龄
开发语言·javascript·ecmascript
Dick5074 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人
黄敬峰5 小时前
从 XMLHttpRequest 到 JSON 模拟:打通前后端通信的任督二脉
javascript
weixin_471383035 小时前
Taro-03-页面生命周期
前端·javascript·taro
Asize5 小时前
数组数据结构底层:从灵活到陷阱
前端·javascript·算法