作为前端开发者,你是不是也遇到过这些问题:
- 想在页面不刷新的情况下获取后端数据,却不知道怎么下手?
- 用XHR写Ajax请求,回调嵌套绕到晕?
- 异步代码执行顺序混乱,console.log先出end再出数据?
这篇文章我会用实战案例,从基础的XHR到现代的async/await,手把手教你3种前端异步请求方式,彻底搞懂JS异步逻辑,搞定前后端数据交互!

一、先搞懂:为什么需要Ajax?
在Web2.0时代,我们要实现页面不刷新就能动态更新内容(比如待办列表、商品数据),就必须用Ajax发起HTTP请求。
核心逻辑:
- JS是单线程语言,遇到异步任务不会阻塞,会丢到eventloop队列
- 等异步任务执行时机到了,eventloop再把任务放回JS线程执行
- Ajax就是最典型的异步场景,核心是「后台请求数据+前端动态渲染」
二、实战场景:搭建本地接口+前端请求渲染待办列表
先搭一个极简的本地Node服务,提供待办列表接口,再用不同方式请求数据渲染页面,全程代码可直接复制运行!
第一步:搭建本地Node服务(提供/todos接口)
新建index.js文件,复制以下代码:
javascript
// 引入Node内置http模块
const http = require("http");
// 创建HTTP服务
http.createServer((req, res) => {
// 模拟待办列表数据
const todos = [
{ id: "1", title: "过四六级", completed: false },
{ id: "2", title: "回家过节", completed: false }
];
// 🔥 踩坑提醒:跨域必加!否则前端请求会报错
res.setHeader("Access-Control-Allow-Origin", "*");
// 告诉前端响应数据是JSON格式
res.setHeader("Content-Type", "application/json; charset=utf-8");
// 接口路由处理
if (req.url === "/") {
res.end("hello world");
}
if (req.url === "/todos") {
// 把对象序列化为JSON字符串(网络传输必须转字符串)
res.end(JSON.stringify(todos));
}
}).listen(3000, () => {
console.log("server is running at 3000 port");
});
运行命令:
bash
node index.js
此时访问http://localhost:3000/todos,就能看到JSON格式的待办数据,接口搭建完成!
第二步:前端页面准备(基础结构)
新建index.html文件,基础结构如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ajax实战</title>
</head>
<body>
<!-- 待办列表容器 -->
<ul class="todo-list"></ul>
<script>
// 后续请求代码写在这里
</script>
</body>
</html>
三、3种Ajax请求方式,从旧到新逐个讲
方式1:传统XHR(XMLHttpRequest)------ 基础但繁琐
这是Ajax的「老前辈」,虽然写法繁琐,但理解它能搞懂异步本质!
把以下代码替换到index.html的script标签中:
javascript
// 1. 实例化XHR对象
const xhr = new XMLHttpRequest();
// 2. 打开通信通道:GET请求 + 接口地址 + 是否异步(true=异步)
xhr.open("GET", "http://localhost:3000/todos", true);
console.log("start");
// 3. 监听状态变化(回调函数)
xhr.onreadystatechange = function() {
// 🔥 踩坑提醒:必须判断状态码,否则会多次触发
if (xhr.readyState === 4 && xhr.status === 200) {
// 把JSON字符串解析为JS对象
const todos = JSON.parse(xhr.responseText);
console.log(todos);
// 渲染到页面
document.querySelector(".todo-list").innerHTML = todos.map(todo => `
<li>${todo.title}</li>
`).join("");
}
};
// 4. 发送请求
xhr.send();
console.log("end");
运行结果&踩坑点:
- console先输出
start→end,再输出todos数据(异步特性) - 必踩坑:忘记判断
readyState === 4和status === 200,会导致代码多次执行 - 缺点:回调嵌套多了会形成「回调地狱」,代码可读性差
方式2:Promise封装(fetch)------ 简化回调
fetch是基于Promise的新一代请求方式,写法更简洁,无需手动监听状态!
替换script标签代码:
javascript
console.log("start");
// fetch返回Promise对象,用then处理异步结果
fetch("http://localhost:3000/todos")
// 第一步:解析响应为JSON
.then(res => res.json())
// 第二步:渲染数据
.then(todos => {
console.log(todos);
document.querySelector(".todo-list").innerHTML = todos.map(todo => `
<li>${todo.title}</li>
`).join("");
})
// 🔥 踩坑提醒:fetch仅捕获网络错误,HTTP错误(如404)需手动判断
.catch(err => console.error("请求失败:", err));
console.log("end");
优点&踩坑点:
- 优点:链式调用替代回调嵌套,代码更清晰
- 踩坑点:fetch不会自动拦截404/500等HTTP错误,需在第一个then里判断
res.ok
方式3:async/await ------ 异步代码「同步化」(推荐)
这是目前最优雅的写法,基于Promise封装,代码和同步逻辑几乎一致,可读性拉满!
替换script标签代码:
javascript
// 封装为async函数
async function getTodos() {
try {
console.log("start");
// 等待请求响应
const res = await fetch("http://localhost:3000/todos");
// 🔥 踩坑提醒:手动判断响应状态
if (!res.ok) {
throw new Error(`HTTP错误:${res.status}`);
}
// 等待解析JSON
const todos = await res.json();
// 渲染数据
document.querySelector(".todo-list").innerHTML = todos.map(todo => `
<li>${todo.title}</li>
`).join("");
console.log(todos);
console.log("end");
} catch (err) {
console.error("请求失败:", err);
}
}
// 执行函数
getTodos();
核心优势:
- 代码线性执行,和同步代码逻辑一致,新手也能看懂
- try/catch统一捕获错误,无需在then里单独处理
- 是目前前端异步请求的「最优解」,大厂项目几乎都用它
四、关键知识点补充(必记)
1. JSON.stringify/JSON.parse
JSON.stringify(obj):把JS对象转为JSON字符串(网络传输必须用字符串)JSON.parse(str):把JSON字符串转回JS对象- 可选参数:
JSON.stringify(value, replace, space)- replace:筛选/修改序列化的属性(null=原样序列化)
- space:设置空格数,提升可读性(团队规范常用2/4个空格)
2. 异步执行核心逻辑
- JS单线程 → 异步任务入eventloop队列 → 主线程空闲后执行
- await会「暂停」async函数执行,等待Promise完成后再继续
- 异步代码执行顺序:同步代码先执行,异步代码后执行(比如前面的start/end输出顺序)
五、总结
今天我们用「待办列表」实战场景,讲了3种Ajax请求方式:
- XHR:基础底层,理解异步本质,但写法繁琐
- fetch+Promise:简化回调,链式调用更清晰
- async/await:异步代码同步化,可读性最高,推荐生产环境使用
核心收获:
- 搞定前后端跨域(设置Access-Control-Allow-Origin)
- 掌握异步代码执行逻辑,避免顺序混乱
- 能独立搭建本地接口+前端请求渲染页面