AJAX 是什么?
- AJAX 全称是 Asynchronous JavaScript and XML (异步 JavaScript 和 XML),核心是在不刷新整个页面的情况下,与服务器异步交换数据,实现页面局部更新 (比如网页的评论加载、搜索建议、数据分页都是典型场景);
- 注意:
- 异步:JS 代码执行时,不会等待 AJAX 请求完成,而是继续执行后续代码,请求完成后再通过回调处理结果;
- 现在 AJAX 不一定用 XML,更多用 JSON 传输数据 (更轻量、易解析);
AJAX 的实现方式
传统 XMLHttpRequest(原生核心)
-
完整示例
// 1. 创建 XMLHttpRequest 对象(核心对象) const xhr = new XMLHttpRequest(); // 2. 配置请求:method(请求方式)、url(接口地址)、async(是否异步,默认 true) xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true); // 3. 设置响应数据格式(可选,推荐加) xhr.responseType = 'json'; // 直接解析为 JSON 对象,无需手动 JSON.parse // 4. 监听请求状态变化(核心:处理响应) xhr.onreadystatechange = function() { // readyState 是请求状态:4 表示响应已完全接收;status 200 表示请求成功 if (xhr.readyState === 4 && xhr.status === 200) { // 成功获取响应数据 console.log('请求成功:', xhr.response); // 比如把数据渲染到页面 document.getElementById('content').innerText = xhr.response.title; } else if (xhr.readyState === 4) { // 请求完成但失败(比如 404、500) console.error('请求失败:', xhr.status, xhr.statusText); } }; // 5. 发送请求(GET 请求无请求体,传 null;POST 需要传数据) xhr.send(null);const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts', true); // POST 请求必须设置请求头(告诉服务器数据格式) xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { console.log('提交成功:', xhr.response); } else { console.error('提交失败:', xhr.status); } } }; // 构造请求体(JSON 字符串) const postData = JSON.stringify({ title: '我的AJAX测试', body: '这是POST请求的测试内容', userId: 1 }); xhr.send(postData); -
XHR 的属性
属性 描述 xhr.response 响应体数据,类型取决于 responseType 的指定 xhr.status 响应状态码值,比如 200、404 ----标识着请求成功或者失败 xhr.statusText 响应状态文本 xhr.timeout 指定请求超时时间,默认为 0 代表没有限制 xhr.withCredentials 值为 true:跨域资源共享中,允许携带资源凭证 xhr.readyState 表示请求状态:
0 创建完 XHR
1 已经完成 OPEN 操作
2 服务器已经把响应头信息返回了
3 响应主体正在返回中
4 响应主体已经返回 -
XHR 的方法
方法 描述 xhr.abort() 强制中断请求 xhr.getAllResponseHeaders() 获取所有请求头 xhr.getResponseHeader() 获取请求头 xhr.setRequestHeader() 设置请求头(属性值不能是中文和特殊字符) xhr.open(method,url[,async]) 初始化一个请求 xhr.overrideMimeType() 重写资源类型 xhr.send() 发送请求 -
XHR 的事件
方法 描述 load 在接收到完整的响应数据时触发 readystatechange 每当 readyState 属性改变时,就会调用该函数 error 在请求发生错误时触发
Fetch API(现代替代方案)
Axios(基于 Promise 封装的第三方库)
HTTP 请求
HTTP 请求:Web 通信的「消息格式」
-
HTTP 请求的完整结构:
请求行 → 方法 路径 协议版本 请求头 → Key: Value(多个) 请求体 → 可选(POST/PUT 等请求才有) -
常用 HTTP 请求方法 (核心)
方法 语义 常用场景 是否有请求体 GET 读取 / 获取资源 查数据、分页、搜索 ❌ POST 创建新资源 提交表单、新增数据 ✅ PUT 全量更新资源 替换整个资源 (比如修改用户所有信息) ✅ PATCH 增量更新资源 只修改资源的某个字段 (比如只改用户名) ✅ DELETE 删除资源 删除数据 ❌ (参数可拼 URL) OPTIONS 预检请求 (跨域时) 浏览器自动发,检测服务器允许的方法 / 头 ❌ -
实际请求示例 (抓包视角)
POST /api/posts HTTP/1.1 Host: jsonplaceholder.typicode.com Content-Type: application/json User-Agent: Chrome/120.0.0.0 Accept: application/json { "title": "测试POST", "body": "请求体数据", "userId": 1 }
HTTP 状态码:服务器的「响应状态」
-
状态码分类 (按第一位数字)
分类 数字范围 含义 典型场景 1xx 100-101 信息性 请求已接收,继续处理 (极少用到) 2xx 200-299 成功 请求已成功处理 3xx 300-399 重定向 需要客户端进一步操作 (比如跳转到新地址) 4xx 400-499 客户端错误 请求有问题 (比如参数错、没权限) 5xx 500-599 服务器错误 服务器处理请求时出错 (比如代码 bug、数据库挂了) -
最常用的状态码:
- 2xx 成功
- 200 OK:最常用,请求成功 (GET/POST/PUT 都可能返回);
- 201 Created:POST 请求创建资源成功 (比如新增用户 / 文章);
- 204 No Content:请求成功,但无返回数据 (比如 DELETE 删除成功);
- 3xx 重定向
- 301 Moved Permanently:永久重定向 (比如域名更换);
- 302 Found:临时重定向 (比如未登录跳登录页);
- 304 Not Modified:资源未修改 (浏览器缓存命中,无需重新下载);
- 4xx 客户端错误
- 400 Bad Request:请求参数错误 (比如格式不对、缺少必填项);
- 401 Unauthorized:未认证 (比如没登录、Token 过期);
- 403 Forbidden:已认证,但无权限 (比如普通用户访问管理员接口);
- 404 Not Found:请求的资源不存在 (比如 URL 输错、接口不存在);
- 405 Method Not Allowed:请求方法不支持 (比如接口只允许 GET,却用了 POST);
- 408 Request Timeout:请求超时 (客户端发请求太慢);
- 429 Too Many Requests:请求过于频繁 (限流 / 防刷);
- 5xx 服务器错误
- 500 Internal Server Error:服务器内部错误 (比如代码抛异常、数据库连接失败);
- 502 Bad Gateway:网关错误 (比如反向代理服务器没收到后端响应);
- 503 Service Unavailable:服务器暂时不可用 (比如维护、过载);
- 504 Gateway Timeout:网关超时 (后端接口响应太慢);
- 2xx 成功
Content-Type:数据的「格式标识」
-
Content-Type (内容类型) 是 HTTP 头字段,核心作用是告诉对方「我传递的数据是什么格式」,分为两种场景:
- 请求头的 Content-Type:客户端告诉服务器「请求体的数据格式」;
- 响应头的 Content-Type:服务器告诉客户端「响应体的数据格式」;
-
最常用的 Content-Type 值:
值 含义 适用场景 数据示例 application/json JSON 格式(最常用) AJAX / 前后端交互 {“name”:“张三”,“age”:20} application/x-www-form-urlencoded 表单默认格式(URL 编码) 普通表单提交 name=张三&age=20 multipart/form-data 多部分表单格式 文件上传(图片 / 视频) 包含二进制文件 + 普通字段 text/plain 纯文本格式 简单文本传输 hello world text/html HTML 格式 服务器返回网页 测试image/jpeg/image/png 图片格式 传输图片文件 二进制图片数据 -
不同 Content-Type 的实际应用:
// fetch POST 请求(JSON 格式) fetch('/api/user', { method: 'POST', headers: { 'Content-Type': 'application/json' // 关键:告诉服务器是 JSON 格式 }, body: JSON.stringify({ name: '张三', age: 20 }) // JSON 字符串 });// 表单格式的 POST 请求 fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=zhangsan&password=123456' // URL 编码字符串 });// 上传文件 const formData = new FormData(); formData.append('file', fileInput.files[0]); // 文件字段 formData.append('name', '测试文件'); // 普通字段 fetch('/api/upload', { method: 'POST', body: formData // 自动设置 Content-Type: multipart/form-data; boundary=xxx });
同源策略-跨域解决方案
面试题
倒计时抢购
如何从服务器获取时间,以及存在的问题:
- 可以基于 ajax 向服务器发送请求,服务器返回的响应头中包含了服务器时间(GMT 格林威治时间,new Date([转换的时间] 转换为北京时间 ));
- 由于网络传送存在时差,导致客户端接收到的服务器时间和真实时间存在偏差,当响应头信息返回(AJAX 状态为 2 的时候),获取时间即可,HTTP 传输中的 HEAD 请求方式,就是只获取响应头的信息;
- 获取当前客户端本地的时间 (但是这个时间客户可以修改自己本地的时间):真实项目中只能做一些参考的工作,不能做严谨的校验,严格校验的情况下,需要的时间是从服务器获取的;
class CountdownTimer {
constructor(targetTimeSelector, targetDate) {
this.targetTime = new Date(targetDate);
this.displayElement = document.querySelector(targetTimeSelector);
this.serverTime = null;
this.timer = null;
this.isRunning = false;
// 错误处理:检查DOM元素是否存在
if (!this.displayElement) {
console.error(`Element with selector "${targetTimeSelector}" not found`);
throw new Error('Target element not found');
}
}
// 格式化时间为 HH:MM:SS
static formatTime(hours, minutes, seconds) {
const pad = num => num.toString().padStart(2, '0');
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
}
// 从服务器获取时间(模拟)
async queryServerTime() {
try {
// 实际项目中这里应该是真实的API调用
const response = await fetch('/api/server-time');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return new Date(data.serverTime);
} catch (error) {
console.warn('Failed to fetch server time, using client time:', error);
// 降级策略:服务器时间获取失败时使用客户端时间
return new Date();
}
}
// 计算时间差并分解
calculateTimeDifference(targetTime, currentTime) {
const diffMs = targetTime - currentTime;
if (diffMs <= 0) {
return {
expired: true,
hours: 0,
minutes: 0,
seconds: 0,
totalMs: 0
};
}
const totalSeconds = Math.floor(diffMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return {
expired: false,
hours,
minutes,
seconds,
totalMs: diffMs
};
}
// 更新显示
updateDisplay(timeData) {
if (timeData.expired) {
this.displayElement.textContent = '00:00:00';
this.displayElement.classList.add('expired');
} else {
this.displayElement.textContent = CountdownTimer.formatTime(
timeData.hours,
timeData.minutes,
timeData.seconds
);
this.displayElement.classList.remove('expired');
}
}
// 单次计算
compute() {
const timeData = this.calculateTimeDifference(this.targetTime, this.serverTime);
this.updateDisplay(timeData);
return timeData;
}
// 定时更新
async updateTimer() {
if (!this.isRunning) return;
// 更新服务器时间(每秒+1000ms)
this.serverTime = new Date(this.serverTime.getTime() + 1000);
const timeData = this.compute();
// 如果时间已到,停止定时器
if (timeData.expired) {
this.stop();
this.onExpired?.();
}
}
// 开始倒计时
async start() {
if (this.isRunning) {
console.warn('Timer is already running');
return;
}
try {
// 获取初始服务器时间
this.serverTime = await this.queryServerTime();
// 初始计算
const initialTimeData = this.compute();
// 如果已经过期,直接返回
if (initialTimeData.expired) {
this.onExpired?.();
return;
}
// 启动定时器
this.isRunning = true;
this.timer = setInterval(() => {
this.updateTimer();
}, 1000);
this.onStart?.();
} catch (error) {
console.error('Failed to start countdown timer:', error);
throw error;
}
}
// 停止倒计时
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.isRunning = false;
this.onStop?.();
}
// 重置倒计时
async reset(newTargetDate = null) {
this.stop();
if (newTargetDate) {
this.targetTime = new Date(newTargetDate);
}
// 重置显示
this.displayElement.textContent = '--:--:--';
this.displayElement.classList.remove('expired');
await this.start();
}
// 销毁实例
destroy() {
this.stop();
this.serverTime = null;
this.displayElement = null;
}
// 事件回调(可选)
onStart = null;
onStop = null;
onExpired = null;
}
// 使用示例
async function initCountdown() {
try {
const timer = new CountdownTimer('#spanBox', '2020/02/25 20:49:30');
// 设置事件回调
timer.onExpired = () => {
console.log('倒计时结束!可以开始抢购了!');
// 触发抢购逻辑
startFlashSale();
};
timer.onStart = () => {
console.log('倒计时开始');
};
// 开始倒计时
await timer.start();
// 如果需要重置
// setTimeout(() => timer.reset('2020/02/26 10:00:00'), 5000);
return timer;
} catch (error) {
console.error('初始化倒计时失败:', error);
// 降级显示
document.querySelector('#spanBox').textContent = '即将开始';
}
}
// 启动倒计时
document.addEventListener('DOMContentLoaded', () => {
const timerInstance = initCountdown();
// 如果需要,可以在页面离开时清理
window.addEventListener('beforeunload', () => {
timerInstance?.then(timer => timer.destroy());
});
});
// 模拟抢购开始函数
function startFlashSale() {
console.log('抢购开始!');
// 这里可以添加开始抢购的逻辑
document.querySelector('#buyButton').disabled = false;
}
readyState 变化几次
let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', true); // readyState 变为 1(仅打开,未发送)
xhr.send(); // 异步请求:将请求任务放入宏任务队列,主线程继续执行同步代码
// 同步代码:此时请求还在宏任务队列,readyState 仍为 1,绑定回调
xhr.onreadystatechange = function () {
console.log(xhr.readyState); //=>2.3.4
};
let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', false); // readyState 变为 1
xhr.send(); // 同步请求:阻塞主线程,直到请求完成(readyState 直接到 4)
// 同步代码:此时 readyState 已经是 4,且无后续变化,绑定回调
xhr.onreadystatechange = function () {
console.log(xhr.readyState); // 无输出
};
let xhr = new XMLHttpRequest;
xhr.open('get', './js/fastclick.js', false); // readyState 变为 1
// 同步代码:此时 readyState 为 1,绑定回调
xhr.onreadystatechange = function () {
console.log(xhr.readyState); //=>4
};
xhr.send(); // 同步请求:阻塞主线程,readyState 从 1 → 4(状态变化触发回调)
第 4️⃣ 座大山:同步异步编程、EventLoop
上一篇