🔥 面试官问"Ajax原理",我从XHR讲到async/await,他直接懵了!
摘要:Ajax 已经过时了?错!你用的每一个「加载更多」、每一个「点赞不刷新」、每一个「搜索联想」,背后都是 Ajax 在默默搬砖。今天带你从零手写一个,彻底搞懂异步编程三代写法!
📌 前言
上周面试,面试官问我:"说说 Ajax 的原理?"
我心想:这还不简单?然后我从 XMLHttpRequest 讲到 Fetch API,从回调地狱讲到 async/await,最后补了个 CORS 跨域...
面试官:停停停,你先说说什么是 Ajax?
我当时就懵了。
后来我花了两天时间,把 Ajax 从头到尾梳理了一遍,写了这篇 保姆级教程。如果你也想彻底搞懂 Ajax,这篇文章就是为你准备的!
🎯 本文适合谁
- 🔰 前端新手:想了解 Ajax 是什么,怎么用
- 💼 求职者:面试被问 Ajax 原理,想系统梳理
- 🧑💻 中级开发者:用过 fetch/axios,但想搞懂底层原理
- 🎓 学生党:正在学习 Web 开发,想打好基础
📚 核心内容
一、Ajax 是个啥?
想象一下没有 Ajax 的世界:你点个赞,整个页面白屏刷新一次;你搜个东西,得跳到新页面等半天。这就是 Web 1.0 的日常------每次交互都是一次「推倒重来」。
Ajax 的出现改变了这一切。它的核心能力就一句话:页面不刷新,偷偷发请求,悄悄更新内容。
💡 通俗理解:你可以把 Ajax 想象成餐厅里的服务员:
- 没有 Ajax:你每加一道菜,厨师都得把整桌菜重做一遍 🍳
- 有了 Ajax:你加菜,服务员悄悄去厨房端来,其他菜纹丝不动 🍽️
常见的两种实现方式:
XMLHttpRequest(XHR)--- 老前辈,2006 年就标准化了,IE7 就支持fetch--- 新生代,2015 年出道,Promise 加身,语法更优雅
二、后端服务搭建(Node.js)
🧊 冷知识 :早期 JS 连模块都没有,所有代码都往 HTML 里塞
<script>标签,项目大了就是一场灾难。后来 Node.js 带来了 CommonJS(require),再后来 ES 标准推出了 ESM(import),JS 终于像门正经语言了。
2.1 创建项目
首先创建一个 backend 文件夹,初始化项目:
bash
mkdir backend
cd backend
npm init -y
2.2 package.json 配置
json
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"type": "commonjs",
"scripts": {
"start": "node index.js"
}
}
⚠️ 注意 :
"type": "commonjs"告诉 Node:用require,别用import。不加这个,后面会报错!
2.3 编写后端服务
javascript
// backend/index.js
const http = require('http');
// 创建 HTTP 服务器,监听请求并返回响应
http.createServer((req, res) => {
const todos = [
{ id: '1', title: '过四六级', completed: false },
{ id: '2', title: '回家过节', completed: false }
];
// 根路径 → 返回纯文本
if (req.url === '/') {
res.end("hello world");
}
// /todos → 返回 JSON 数据
if (req.url === '/todos') {
// 设置 CORS 头,允许跨域(不加这个,前端会报 CORS 错误然后来骂你)
res.setHeader('Access-Control-Allow-Origin', '*');
// 设置响应内容类型
res.setHeader('Content-Type', 'application/json; charset=utf-8');
// 将 JS 对象序列化为 JSON 字符串后返回
res.end(JSON.stringify(todos));
}
}).listen(3000, () => {
console.log('server is running at 3000 port');
});
2.4 启动服务
bash
node index.js
# 输出:server is running at 3000 port
💡 验证 :打开浏览器访问
http://localhost:3000/todos,应该能看到 JSON 数据。
三、前端 Ajax 请求(XMLHttpRequest)
3.1 创建前端项目
创建 frontend 文件夹,新建 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>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
ul { list-style: none; padding: 0; }
li {
padding: 10px;
margin: 5px 0;
background: #f5f5f5;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>📝 Todo 列表</h1>
<ul id="todos"></ul>
<script>
// ===== XMLHttpRequest 使用流程:new → open → 设置回调 → send =====
// 1. 创建 XHR 实例(就像打电话前先拿手机)
const xhr = new XMLHttpRequest();
// 2. 初始化请求(拨号:告诉它打给谁、用什么方式)
xhr.open('GET', 'http://localhost:3000/todos', true);
console.log('start'); // 验证异步
// 3. 注册状态变化回调(等电话接通,随时准备说话)
xhr.onreadystatechange = function () {
console.log('readyState:', xhr.readyState);
// status=200 请求成功,readyState=4 响应完毕
if (xhr.status === 200 && xhr.readyState === 4) {
// JSON 字符串 → JS 对象(把对方说的话翻译成你能理解的语言)
const todos = JSON.parse(xhr.responseText);
// 映射为 HTML 并拼接,渲染到页面(把数据变成好看的界面)
document.getElementById('todos').innerHTML = todos
.map(todo => `<li>✅ ${todo.title}</li>`)
.join('');
}
};
// 4. 发送请求(按下拨号键)
xhr.send();
console.log('end'); // 验证异步:先于回调执行
</script>
</body>
</html>
3.2 运行效果
bash
# 1. 先启动后端
node backend/index.js
# 2. 用浏览器打开 frontend/index.html
# 3. 控制台输出顺序:
# start → end → readyState: 1 → readyState: 2 → readyState: 3 → readyState: 4
🤔 等等,为什么
start和end先打印,1→2→3→4后打印?这就是异步的魅力 ------
send()发完请求就走了,不等回复,像个发了消息就放下手机的人。
四、异步执行顺序详解
makefile
主线程: start → send() → end (我先忙别的去了)
回调队列: readyState: 1→2→3→4 → 渲染 DOM (数据回来了再处理)
🎯 通俗理解:外卖点餐
就像你点了外卖:
- 下单 (
send())→ 告诉商家你要什么 - 继续打游戏(主线程继续执行)→ 不用傻等
- 外卖到了,暂停游戏去取餐(回调触发)→ 数据回来再处理
而不是下单后站在门口傻等(同步阻塞)。
📊 XHR readyState 状态说明
| readyState | 含义 | 大白话 |
|---|---|---|
| 0 UNSENT | 已创建,未调用 open() |
手机拿起来了,还没拨号 |
| 1 OPENED | 已调用 open() |
号拨出去了,等对方接 |
| 2 HEADERS_RECEIVED | 已收到响应头 | 对方接了,说了句「喂」 |
| 3 LOADING | 正在接收响应体 | 对方在说话,还没说完 |
| 4 DONE | 响应接收完毕 | 对方说完了,你可以挂了 |
五、XHR vs Fetch 对比
| 特性 | XHR(老前辈) | Fetch(新生代) |
|---|---|---|
| 语法风格 | 回调地狱选手 | Promise 优雅选手 |
| 浏览器兼容 | IE7+ 老当益壮 | 现代浏览器专属 |
| 取消请求 | xhr.abort() 简单粗暴 |
AbortController 略显复杂 |
| 进度监听 | ✅ onprogress 天生支持 |
得自己想办法 |
| 代码量 | 多到想哭 | 少到想笑 |
💡 选谁? 新项目用
fetch,需要兼容 IE 或要进度条的用xhr,想省心就用axios(封装了 XHR,还附赠拦截器)。
六、async/await:异步的终极进化
6.1 为什么需要 async/await?
前面我们见识了两种异步写法:
XHR 的回调风格(2006 年)------ 嵌套多了就是回调地狱:
javascript
// ❌ 回调地狱:嵌套太深,眼睛已瞎 👀
xhr.onreadystatechange = function () {
if (xhr.status === 200 && xhr.readyState === 4) {
const user = JSON.parse(xhr.responseText);
xhr2.open('GET', '/orders?userId=' + user.id);
xhr2.onreadystatechange = function () {
if (xhr2.status === 200 && xhr2.readyState === 4) {
const orders = JSON.parse(xhr2.responseText);
xhr3.open('GET', '/products?orderId=' + orders[0].id);
// 继续嵌套... 😵
}
};
}
};
Fetch 的 Promise 风格 (2015 年)------ 用 .then() 链式调用,好多了:
javascript
// ✅ Promise 链式调用:比回调好,但还是有点绕
fetch('/user')
.then(res => res.json())
.then(user => fetch('/orders?userId=' + user.id))
.then(res => res.json())
.then(orders => fetch('/products?orderId=' + orders[0].id))
.then(res => res.json())
.then(products => console.log(products))
.catch(err => console.error(err));
async/await 风格(2017 年)------ 让异步代码看起来像同步一样:
javascript
// ✅ async/await:像写同步代码一样优雅
async function getData() {
try {
const user = await fetch('/user').then(r => r.json());
const orders = await fetch('/orders?userId=' + user.id).then(r => r.json());
const products = await fetch('/products?orderId=' + orders[0].id).then(r => r.json());
console.log(products);
} catch (err) {
console.error(err);
}
}
🚗 感觉就像:从「用脚蹬三轮车」→「骑自行车」→「开汽车」的进化。代码越来越像人话了。
6.2 async/await 到底是什么?
一句话:async/await 是 Promise 的语法糖,让异步代码写起来像同步代码。
async放在函数前面,表示这个函数里有异步操作await放在 Promise 前面,表示「等这个操作完成再继续」
javascript
// 没有 async/await:得用 .then() 链式处理
fetch('/todos')
.then(response => response.json())
.then(data => console.log(data));
// 有 async/await:像写同步代码一样
async function getTodos() {
const response = await fetch('/todos'); // 等请求完成
const data = await response.json(); // 等解析完成
console.log(data); // 两个都完成了才执行
}
6.3 实战:用 async/await 改写 todo 项目
html
<script>
// async 函数:告诉 JS「这个函数里有 await,别卡死我」
async function loadTodos() {
try {
// await:等 fetch 完成,拿到 response 对象
const response = await fetch('http://localhost:3000/todos');
// await:等 json() 解析完成,拿到数据
const todos = await response.json();
// 渲染到页面
document.getElementById('todos').innerHTML = todos
.map(todo => `<li>✅ ${todo.title}</li>`)
.join('');
} catch (err) {
// 网络错误、JSON 解析错误都会进这里
console.error('请求失败:', err);
}
}
// 调用 async 函数
loadTodos();
</script>
6.4 错误处理:try...catch vs .catch()
javascript
// Promise 写法:错误处理和业务逻辑分开,容易漏
fetch('/todos')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err)); // 错误处理在最后
// async/await 写法:try...catch 包裹,一目了然
async function loadTodos() {
try {
const res = await fetch('/todos');
const data = await res.json();
console.log(data);
} catch (err) {
// 任何一步出错都会被捕获
console.error(err);
}
}
🎯 记住:try...catch 是个安全网,不管哪一步摔了都能接住。
6.5 并发请求:Promise.all 登场
有时候你需要同时发多个请求,等它们全部完成:
javascript
// ❌ 串行:一个一个等(慢!每个请求都要等前一个完成)
async function slow() {
const users = await fetch('/users').then(r => r.json());
const posts = await fetch('/posts').then(r => r.json());
const comments = await fetch('/comments').then(r => r.json());
// 总耗时 = users + posts + comments
}
// ✅ 并发:同时发,等最慢的那个(快!)
async function fast() {
const [users, posts, comments] = await Promise.all([
fetch('/users').then(r => r.json()),
fetch('/posts').then(r => r.json()),
fetch('/comments').then(r => r.json())
]);
// 总耗时 = max(users, posts, comments)
}
🍕 通俗理解:就像你同时点了三份外卖,等最慢的那份到了就能开饭。而不是一份到了再点下一份。
6.6 三种写法进化史一览
javascript
// ===== 1. XHR 回调(2006)=====
const xhr = new XMLHttpRequest();
xhr.open('GET', '/todos');
xhr.onreadystatechange = function () {
if (xhr.status === 200 && xhr.readyState === 4) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
// ===== 2. Fetch + .then()(2015)=====
fetch('/todos')
.then(res => res.json())
.then(data => console.log(data));
// ===== 3. async/await(2017)=====
async function getTodos() {
const res = await fetch('/todos');
const data = await res.json();
console.log(data);
}
getTodos();
📈 代码越来越少,可读性越来越高。这就是 JS 异步编程的进化之路。
6.7 注意事项
1. await 只能在 async 函数里用
javascript
// ❌ 报错!顶层不能直接用 await(除非在 ES Module 中)
const data = await fetch('/todos');
// ✅ 正确:包在 async 函数里
async function main() {
const data = await fetch('/todos');
}
2. await 会暂停当前函数,但不会阻塞主线程
javascript
async function demo() {
console.log('A');
await fetch('/todos'); // 函数暂停在这里
console.log('B'); // 等 fetch 完成后才执行
}
demo();
console.log('C');
// 输出顺序:A → C → B
// 函数暂停了,但外面的代码继续跑
3. 别忘了错误处理
javascript
// ❌ 危险:没有 try...catch,请求失败程序直接崩
async function risky() {
const res = await fetch('/todos');
const data = await res.json();
}
// ✅ 安全:用 try...catch 兜底
async function safe() {
try {
const res = await fetch('/todos');
const data = await res.json();
} catch (err) {
console.error('出错了:', err);
}
}
💡 重点总结
🧠 核心知识回顾
http模块 :createServer创建服务,listen监听端口------Node.js 的 Hello World- XHR 生命周期 :
new → open → onreadystatechange → send------四步走,记住就完事 - 异步进化:回调 → Promise → async/await------代码越来越像人话
- 数据流转 :
JSON.stringify序列化 → 网络传输 →JSON.parse反序列化------前后端的翻译官
💡 从这个小项目中我们悟到了什么
1. 前后端分离 = 各干各的,用 JSON 传纸条
前端只管画界面,后端只管存数据,两者通过 HTTP + JSON 通信。
2. JSON 是前后端的普通话
后端说 JS 对话,前端也说 JS 话,但中间传输只能传字符串。所以需要 JSON.stringify() 把对象「压缩」成字符串发过去,对面再用 JSON.parse()「解压」还原。
3. 异步是 JS 的超能力
send() 发出请求后不等结果就继续执行,这在其他语言里可能要用多线程,但 JS 天生就是异步的。
4. XHR 虽老,但它是你的老师
fetch 把很多细节封装了,你用着爽但不知道底层发生了什么。XHR 每一步都摆在明面上,学完它再用 fetch,就像学完手动挡再开自动挡------你知道它在帮你做什么。
🎯 实际开发怎么选
| 场景 | 推荐方案 | 一句话理由 |
|---|---|---|
| 新项目 | fetch + async/await |
原生的,不用装包 |
| 需要拦截器/取消请求 | axios |
功能全家桶 |
| 上传文件要进度条 | XMLHttpRequest |
onprogress 天生支持 |
| 老项目兼容 IE | axios |
fetch 和 IE 不熟 |
🗺️ 下一步去哪
csharp
本文(XHR + Fetch + async/await) ← 你在这里,已解锁现代写法
↓
axios 封装与拦截器 ← 解锁工程化
↓
RESTful API 设计规范 ← 解锁后端思维
↓
WebSocket 实时通信 ← 解锁聊天室/实时推送
↓
GraphQL 查询语言 ← 解锁精准查询
🔗 参考资料
| API | MDN 链接 |
|---|---|
| XMLHttpRequest | developer.mozilla.org/zh-CN/docs/... |
| Fetch API | developer.mozilla.org/zh-CN/docs/... |
| async function | developer.mozilla.org/zh-CN/docs/... |
| await | developer.mozilla.org/zh-CN/docs/... |
| Promise | developer.mozilla.org/zh-CN/docs/... |
| Promise.all() | developer.mozilla.org/zh-CN/docs/... |
| JSON.stringify() | developer.mozilla.org/zh-CN/docs/... |
| JSON.parse() | developer.mozilla.org/zh-CN/docs/... |
| CORS 跨域资源共享 | developer.mozilla.org/zh-CN/docs/... |
💬 交流讨论
读完这篇,你对 Ajax 有什么新的理解?欢迎在评论区讨论!
面试官问你 Ajax 原理,你会怎么回答?
觉得有用?点个赞👍收藏⭐关注👆,下次更新更多前端干货!
📢 下一篇预告:《axios 从入门到精通:拦截器、取消请求、并发请求全搞定》