作为前端开发者,你是不是也遇到过这些问题:
- 想在页面不刷新的情况下获取后端数据,却不知道怎么下手?
- 用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文件,复制以下代码:
// 引入Node内置http模块consthttp=require("http");// 创建HTTP服务http.createServer((req,res)=>{// 模拟待办列表数据consttodos=[{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");});运行命令:
nodeindex.js此时访问http://localhost:3000/todos,就能看到JSON格式的待办数据,接口搭建完成!
第二步:前端页面准备(基础结构)
新建index.html文件,基础结构如下:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Ajax实战</title></head><body><!-- 待办列表容器 --><ulclass="todo-list"></ul><script>// 后续请求代码写在这里</script></body></html>三、3种Ajax请求方式,从旧到新逐个讲
方式1:传统XHR(XMLHttpRequest)—— 基础但繁琐
这是Ajax的「老前辈」,虽然写法繁琐,但理解它能搞懂异步本质!
把以下代码替换到index.html的script标签中:
// 1. 实例化XHR对象constxhr=newXMLHttpRequest();// 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对象consttodos=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标签代码:
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标签代码:
// 封装为async函数asyncfunctiongetTodos(){try{console.log("start");// 等待请求响应constres=awaitfetch("http://localhost:3000/todos");// 🔥 踩坑提醒:手动判断响应状态if(!res.ok){thrownewError(`HTTP错误:${res.status}`);}// 等待解析JSONconsttodos=awaitres.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)
- 掌握异步代码执行逻辑,避免顺序混乱
- 能独立搭建本地接口+前端请求渲染页面