面试官问"Ajax原理",我从XHR讲到async/await,他直接懵了!

🔥 面试官问"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

🤔 等等,为什么 startend 先打印,1→2→3→4 后打印?

这就是异步的魅力 ------send() 发完请求就走了,不等回复,像个发了消息就放下手机的人。


四、异步执行顺序详解

makefile 复制代码
主线程:    start → send() → end        (我先忙别的去了)
回调队列:              readyState: 1→2→3→4 → 渲染 DOM   (数据回来了再处理)
🎯 通俗理解:外卖点餐

就像你点了外卖:

  1. 下单send())→ 告诉商家你要什么
  2. 继续打游戏(主线程继续执行)→ 不用傻等
  3. 外卖到了,暂停游戏去取餐(回调触发)→ 数据回来再处理

而不是下单后站在门口傻等(同步阻塞)。

📊 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);
    }
}

💡 重点总结

🧠 核心知识回顾

  1. http 模块createServer 创建服务,listen 监听端口------Node.js 的 Hello World
  2. XHR 生命周期new → open → onreadystatechange → send------四步走,记住就完事
  3. 异步进化:回调 → Promise → async/await------代码越来越像人话
  4. 数据流转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 从入门到精通:拦截器、取消请求、并发请求全搞定》

相关推荐
兰令水1 小时前
leecodecode【面试150】【2026.6.15打卡-java版本】
java·算法·面试
前端不太难1 小时前
Agent First:鸿蒙 App 的下一代 AI Runtime 架构
人工智能·架构·harmonyos
Chelsea05221 小时前
PC浏览器在线调试 Android 浏览器教程-chrome://inspect/#devices
android·前端·chrome
加号31 小时前
【C#】VS2022 传统 ASP.NET Web 服务(.asmx)接口实现指南
前端·c#·asp.net
AI人工智能_电脑小能手1 小时前
【大白话说Java面试题 第106题】【并发篇】第6题:synchronized 锁的锁对象可以是什么?
java·后端·面试
Rain5091 小时前
2.3. 安全配置:环境变量与 API 密钥管理
前端·人工智能·后端·安全·ai·node.js·ai编程
用户938515635071 小时前
HTML5 Canvas 从入门到AI驱动游戏开发:手把手教你用原生JS打造飞机游戏与数据可视化
前端·javascript·人工智能
William_Xu1 小时前
var [a, b] = { a: 1, b: 2 } 解构赋值
前端
用户059540174461 小时前
Playwright 网络拦截踩坑实录:我花了 3 小时才搞懂数据持久化验证的正确姿势
前端·css