从零讲透 Ajax:XHR、Fetch、JSON 序列化到异步编程全链路

前言

前后端分离的时代,Ajax 是前端工程师每天都要打交道的技术。但很多同学对 Ajax 的理解停留在"调个接口、拿个数据"的表面,对底层发生了什么一知半解。

这篇文章用一个最小的 Node.js 后端 + 原生前端 项目,把 Ajax 全链路串起来讲清楚------从服务端怎么响应请求,到客户端怎么发请求,再到 JS 异步编程的三种写法,以及 JSON 序列化的细节,全部覆盖。

读完你会对"前端请求一个接口"这件事,有从里到外的完整理解。


一、先跑起来:项目全貌

项目结构极其简单,两个文件:

bash 复制代码
ajax/
├── backend/
│   ├── index.js        # Node.js 后端服务
│   └── package.json
└── frontend/
    └── index.html       # 前端页面

后端 :用 Node.js 内置的 http 模块起一个服务,暴露 /todos 接口,返回一组待办事项的 JSON 数据。

前端:用原生 XMLHttpRequest 发异步请求拿数据,动态渲染到页面上。

我们先看后端代码。


二、后端:用 Node.js 原生 http 模块提供 API

bash 复制代码
const http = require('http');

http.createServer((req, res) => {
    const todos = [{
        id: "1",
        title: "过四六级",
        completed: false
    }, {
        id: "2",
        title: "回家过节",
        completed: false
    }]

    if (req.url === '/') {
        res.end("hello world");
    }
    if (req.url === '/todos') {
        res.setHeader('Access-Control-Allow-Origin', '*')
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.end(JSON.stringify(todos));
    }

}).listen(3000, () => {
    console.log(`server is running on 3000 port`);
})

2.1 require vs import------JS 模块化的一小段历史

你可能会好奇为什么用 require 而不是 import。这背后是 JS 模块化演进的一个缩影:

  • 早期前端没有模块化 :所有代码写在全局作用域,靠 <script> 标签引入,变量冲突是家常便饭。
  • Node.js 带来了 CommonJSrequire + module.exports,让 JS 第一次有了正经的模块系统。
  • ESM(ES Modules)是升级版import + export default,静态分析更友好,支持 tree-shaking,现在是标准写法。

这里用 require 是因为 package.json 里指定了 "type": "commonjs",保持早期 Node 项目的风格。实际生产中更推荐 ESM。

2.2 http.createServer 做了什么

http 是 Node.js 内置模块,不需要安装。createServer 接收一个回调函数,每个请求到来时都会调用这个函数。

  • req(IncomingMessage):包含请求信息------URL、请求头、请求方法等。
  • res(ServerResponse):用来构造响应------设置响应头、写入响应体、结束响应。

2.3 关键的响应头设置

bash 复制代码
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Content-Type', 'application/json; charset=utf-8');
  • Access-Control-Allow-Origin: *:经典 CORS 头。浏览器默认禁止跨域请求,加上这个头告诉浏览器"这个接口允许任何来源的请求"。没有它,前端页面在本地打开时无法调通接口。

  • Content-Type: application/json; charset=utf-8:告诉客户端"我返回的是 JSON 格式,用 UTF-8 编码"。这样客户端才能正确解析。

2.4 JSON.stringify------对象到字符串的桥梁

这是本文第一个重点。网络传输只能传二进制/文本,不能传 JS 对象。JSON.stringify 负责把 JS 对象序列化为 JSON 字符串。

bash 复制代码
res.end(JSON.stringify(todos));

它的完整签名是:

bash 复制代码
JSON.stringify(value, replacer?, space?)

三个参数各有讲究:

参数 作用 常见用法
value 要序列化的对象 必传,没啥好说
replacer 控制哪些属性参与序列化 null 原样输出;传数组可以只序列化指定 key;传函数可以自定义转换逻辑
space 缩进空格数 24 让输出格式化,适合调试和团队规范要求可读性的场景

举个简单例子感受 replacerspace 的效果:

bash 复制代码
const obj = { name: "张三", age: 25, password: "123456" };

// replacer 传数组------只要 name 和 age,过滤掉敏感字段
JSON.stringify(obj, ["name", "age"], 2);
// 输出:
// {
//   "name": "张三",
//   "age": 25
// }

// replacer 传 null,space 传 2------原样序列化 + 格式化
JSON.stringify(obj, null, 2);
// 输出:
// {
//   "name": "张三",
//   "age": 25,
//   "password": "123456"
// }

// 不传 space------压缩输出,适合生产环境减少带宽
JSON.stringify(obj);
// 输出:{"name":"张三","age":25,"password":"123456"}

总结replacer 做取舍(过滤字段),传 null 原样序列化。space 给缩进让输出可读,开发调试用得多,生产环境一般不传以节省带宽。


三、前端:XMLHttpRequest 发起异步请求

后端跑在 localhost:3000,现在看前端怎么调它:

bash 复制代码
<ul id="todos"></ul>
<button id="btn">按钮</button>

<script>
console.log('start');

// 注册按钮点击事件
document.getElementById('btn')
    .addEventListener('click', () => {
        console.log('点击按钮');
    })

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/todos', true);

xhr.onreadystatechange = function() {
    console.log(xhr.readyState);
    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();

console.log('end')
</script>

3.1 XHR 的生命周期

XHR(XMLHttpRequest)是 Ajax 的鼻祖,整个流程分四步:

  1. new XMLHttpRequest():实例化一个 XHR 对象,这是发起 Ajax 请求的载体。
  2. xhr.open(method, url, async) :打开一个 HTTP 通道。第三个参数 true 表示异步请求 ------这是 Ajax 的灵魂,如果是同步(false),浏览器会卡死直到响应返回。
  3. xhr.onreadystatechange:注册回调函数,监听请求状态变化。
  4. xhr.send():真正发送请求。

3.2 readyState 的五种状态

onreadystatechange 会在请求状态变化时被触发,readyState 有 5 个值:

readyState 含义 说明
0 UNSENT open() 还没调用
1 OPENED open() 已调用
2 HEADERS_RECEIVED 收到响应头
3 LOADING 正在接收响应体
4 DONE 请求完成
bash 复制代码
// 只有 status === 200 且 readyState === 4 时,
// 才表示请求成功完成,可以安全地处理响应数据
if (xhr.status === 200 && xhr.readyState === 4) {
    const todos = JSON.parse(xhr.responseText);
    // 渲染数据到页面
}

3.3 JSON.parse------字符串回到对象

后端用 JSON.stringify 序列化,前端用 JSON.parse 反序列化,把 JSON 字符串还原为 JS 对象。这一对方法是前后端数据交换的基石。

3.4 Fetch------XHR 的现代替代

代码里还留了一段被注释掉的 fetch 写法:

bash 复制代码
fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })

fetch 是 XHR 的现代化升级:

  • Promise 风格,告别回调地狱
  • 语法更简洁,语义更清晰
  • 但需要注意:res.json() 又返回一个 Promise,需要再 .then() 一次才能拿到数据

XHR 和 fetch 的底层本质是一样的:JS 主动发起 HTTP 请求,拿到数据后动态更新页面。这就是 Web 2.0 时代的核心------页面不用全部刷新,数据异步加载,用户体验飞跃。


四、JS 异步编程:从 Callback 到 async/await

这是本文第二个重点,也是面试高频考点。

4.1 为什么需要异步

JS 是单线程语言,同一时间只能做一件事。如果网络请求是同步的,发请求期间整个页面会冻结,用户什么都点不了。

JS 的解决方案:遇到异步任务(网络请求、定时器、事件监听)时,不等待它完成,而是把它丢到 Event Loop 里,继续往下执行。等异步任务有了结果,再从 Event Loop 里拿出来执行对应的回调。

看这段代码的执行顺序:

bash 复制代码
console.log('start');  // ① 先打印

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/todos', true);
xhr.onreadystatechange = function() {
    if (xhr.status === 200 && xhr.readyState === 4) {
        console.log('数据回来了');  // ③ 最后打印(等网络请求完成)
    }
}
xhr.send();

console.log('end');    // ② 紧接着打印,不会等 XHR 返回

输出顺序是:startend数据回来了,而不是直觉上的 start数据回来了end。这就是异步的力量。

4.2 三种异步写法的演进

JS 异步编程经历了三代演进:

第一代:回调函数(Callback)
bash 复制代码
xhr.onreadystatechange = function() {
    if (xhr.status === 200 && xhr.readyState === 4) {
        // 在这里处理数据
    }
}

最早的写法,直接把"数据到了之后做什么"写成回调函数。问题也很明显------回调地狱:层层嵌套,代码向右无限延伸,可读性灾难。

第二代:Promise + then
bash 复制代码
fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })

Promise 把异步任务封装 成一个对象,通过 .then() 链式调用,解决了回调地狱。代码从左向右延伸变成从上向下延伸,可读性大幅提升。

第三代:async/await(最推荐)
bash 复制代码
async function loadTodos() {
    const res = await fetch('http://localhost:3000/todos');
    const data = await res.json();
    console.log(data);
}

async/await 是最优解:

  • 写法跟同步代码几乎一模一样 ,没有回调嵌套,没有 .then() 链条
  • 错误处理可以直接用 try/catch,而 Promise 的 .catch() 需要单独写
  • 调试友好,断点可以像同步代码一样逐行停住

建议 :新项目一律用 async/await,老项目逐步迁移。


五、全链路串联:一张图看懂 Ajax

把以上所有知识点串起来,一次完整的 Ajax 请求经历了这些步骤:

bash 复制代码
[浏览器]                                      [Node.js 服务器]
   |                                               |
   |  1. xhr.open('GET', '/todos', true)           |
   |  2. xhr.send()  ------------ HTTP 请求 ---------------------→       |
   |                                               |
   |                                    3. http.createServer 回调触发
   |                                    4. 根据 req.url 匹配路由
   |                                    5. JSON.stringify(todos)
   |                                    6. 设置 CORS + Content-Type 头
   |                                               |
   |  7. onreadystatechange 回调触发 ←--------- 响应 ------------ |
   |  8. readyState 从 0 → 1 → 2 → 3 → 4          |
   |  9. JSON.parse(xhr.responseText)              |
   |  10. 动态渲染 DOM                              |
   |                                               |
用户看到最新数据

整个过程的本质就一句话:JS 单线程通过 Event Loop 机制,用异步非阻塞的方式发起 HTTP 请求,拿到 JSON 数据后动态更新 DOM,实现无刷新页面更新。


六、总结

这篇文章覆盖了 Ajax 全链路的关键知识点:

知识点 核心要点
JSON.stringify / JSON.parse 序列化与反序列化,replacer 过滤字段,space 控制格式
XHR Ajax 鼻祖,readyState 五种状态,open() + send() + 回调
Fetch XHR 的现代化替代,Promise 风格,链式调用
JS 异步编程 单线程 + Event Loop → Callback → Promise → async/await
Node.js http 模块 createServerreq/res,CORS 头,Content-Type
CORS Access-Control-Allow-Origin 解决跨域

从最底层的 Event Loop 到最上层的 async/await,从服务端的 JSON.stringify 到客户端的 JSON.parse,从 Node.js 的 http 模块到浏览器的 XHR------当你把这些全部串起来,"前端请求一个接口"就不再是一个黑盒操作,而是一张清晰的蓝图。


相关推荐
yyt3630458411 小时前
KlineChartQuant Tooltip 高频交互 200FPS 性能优化完整复盘
前端·经验分享·性能优化·typescript·vue·交互·chrome devtools
2601_963771372 小时前
WordPress SEO Guide: Boost Rankings Without Technical Jargon
前端·php
李顿波2 小时前
Chrome 扩展程序 MV3 架构组件概览
前端·chrome·浏览器插件
KingCodeHan2 小时前
ESLint 完全指南:从入门到精通
前端
GHL2842710902 小时前
安装chrome浏览器
前端·chrome
谙忆10242 小时前
WebCodecs 实战:用 ImageDecoder 和 VideoFrame 在浏览器里做硬件加速的帧处理
前端
JerrySir2 小时前
密码没泄露,2FA 也没失效:Chrome 146 为什么还要让 Cookie“搬不走”?
前端
小蚂蚁i2 小时前
React Hooks 原理深度解析:从会用到真正懂
前端·react.js
触底反弹2 小时前
Vue 实战:手把手教你实现 ChatGPT 同款打字机效果
前端·javascript·人工智能