你是不是也遇到过这种尴尬场景?代码里写了三个请求,分别去拉用户信息、订单列表和推荐商品。结果页面上,用户名字段突然显示了订单数据,或者推荐商品变成了用户ID。你盯着屏幕发呆,检查了半天代码,发现每个请求的接口都返回了正确的JSON,但就是乱了套。
别急着怀疑人生,这事儿真不怪你。今天咱们就把这个坑彻底填平,从“为什么会乱”讲到“怎么彻底控制”,最后给出一套能直接搬进项目里的解决方案。
为什么并发请求会“打架”?
先别急着看代码,咱们先回到最根本的地方:HTTP请求到底是什么?
想象一下,你去餐厅吃饭。你点了三道菜,服务员分别去厨房下单。这三道菜做完的时间不一样,可能汤先好,也可能主菜先好。但问题来了——如果餐厅只有一种上菜方式,那就是“谁先做完就给谁上”,而且上桌的时候,盘子是混在一起的,你根本不知道哪盘菜对应哪个订单。
在前端AJAX的世界里,这种情况发生得更隐蔽。
1. 共享状态的陷阱
很多初学者会犯这样一个错误:
let result = {};
$.ajax({
url: '/api/user',
success: function(data) {
result.userInfo = data;
console.log(result); // 可能打印的是订单数据!
}
});
$.ajax({
url: '/api/order',
success: function(data) {
result.orderInfo = data;
console.log(result); // 此时userInfo可能还没返回
}
});
这里的问题很明显:result 是一个共享对象。如果 /api/order 先返回,而 /api/user 还没返回,result.userInfo 还是 undefined。更糟糕的是,如果你在 success 回调里做了一些依赖 result 的操作,比如渲染页面,那你渲染的就是一个半成品数据。
2. 请求标识被覆盖
另一个经典错误是复用请求ID或索引:
for (let i = 0; i < 5; i++) {
let index = i;
$.ajax({
url: `/api/item/${i}`,
success: function(data) {
// 这里 index 可能已经不是 i 了!
renderItem(index, data);
}
});
}
这段代码看起来没问题,但如果你用的是旧版JavaScript(没有 let/const),index 会共享同一个作用域,等回调执行时,index 已经是 5 了。即便你用了 let,如果多个请求并发,你也无法保证 renderItem(index, data) 的执行顺序和请求发送顺序一致。
3. 响应数据类型的混淆
有些接口返回的数据结构长得很像,比如:
// /api/user 返回
{ id: 1, name: "张三", age: 25 }
// /api/product 返回
{ id: 1, name: "手机", price: 999 }
如果你在 success 回调里没有严格校验数据类型,直接把 data.name 渲染到页面上,就会发生“用户名字段显示了产品名称”的诡异现象。
核心解决方案:请求标识唯一标记
解决上述问题的第一步,是给每个请求一个独一无二的身份证。这个身份证不是随便写的,它需要包含足够的信息,让你能在回调里准确识别出“这是哪个请求的响应”。
3.1 使用唯一ID生成器
// 一个简单的唯一ID生成器
let requestCounter = 0;
function generateRequestId() {
return `req_${Date.now()}_${++requestCounter}`;
}
每次发起请求时,都生成一个唯一的ID,并把这个ID和请求元数据绑定在一起:
function fetchUserData(userId) {
const requestId = generateRequestId();
return new Promise((resolve, reject) => {
$.ajax({
url: `/api/user/${userId}`,
success: function(data) {
// 验证响应是否属于这个请求(防止被覆盖)
if (data.requestId === requestId) {
resolve(data);
} else {
reject(new Error(`响应ID不匹配: 期望 ${requestId}, 实际 ${data.requestId}`));
}
},
error: function(xhr, status, error) {
reject(new Error(`请求 ${requestId} 失败: ${error}`));
}
});
});
}
3.2 在请求头中携带标识
更优雅的做法是在HTTP请求头中携带唯一标识,这样后端也可以记录,方便排查问题:
function fetchWithTracking(endpoint, options = {}) {
const requestId = generateRequestId();
const defaultOptions = {
headers: {
'X-Request-Id': requestId,
'X-Trace-ID': generateRequestId() // 用于链路追踪
},
timeout: 5000
};
return $.ajax({
url: endpoint,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
}).then(response => {
// 在返回的数据中注入请求ID,方便后续处理
return {
...response,
requestId,
timestamp: Date.now()
};
});
}
状态机管理并发控制
有了唯一标识,接下来我们要解决的是“并发控制”问题。很多时候,我们不是不想并发,而是不能无限制地并发。比如,一个页面上有10个组件都要加载数据,如果同时发10个请求,可能会把服务器打爆,也可能导致浏览器卡顿。
这时候,状态机就派上用场了。
4.1 什么是状态机?
状态机(State Machine)是一种数学模型,它描述了一个系统在某一时刻处于某种状态,并在外部事件触发下从一个状态转移到另一个状态。
在我们的场景中,每个请求可以处于以下几种状态:
- PENDING(待处理):请求已创建,但尚未发送
- SENT(已发送):请求已发送,正在等待响应
- SUCCESS(成功):请求成功返回
- ERROR(失败):请求失败
- CANCELLED(已取消):请求被主动取消
4.2 实现一个简单的请求状态管理器
class RequestState {
constructor(requestId, config) {
this.requestId = requestId;
this.config = config;
this.status = 'PENDING';
this.data = null;
this.error = null;
this.timestamp = Date.now();
this.callbacks = {
success: [],
error: [],
complete: []
};
}
// 转移到已发送状态
setSent() {
this.status = 'SENT';
this.sendTime = Date.now();
}
// 转移到成功状态
resolve(data) {
if (this.status === 'SENT') {
this.status = 'SUCCESS';
this.data = data;
this.endTime = Date.now();
this.duration = this.endTime - this.sendTime;
this.callbacks.success.forEach(cb => cb(data));
this.callbacks.complete.forEach(cb => cb(data));
}
}
// 转移到失败状态
reject(error) {
if (this.status === 'SENT' || this.status === 'PENDING') {
this.status = 'ERROR';
this.error = error;
this.endTime = Date.now();
this.callbacks.error.forEach(cb => cb(error));
this.callbacks.complete.forEach(cb => cb(error));
}
}
// 转移到已取消状态
cancel() {
if (this.status === 'PENDING' || this.status === 'SENT') {
this.status = 'CANCELLED';
this.endTime = Date.now();
this.callbacks.complete.forEach(cb => cb(null));
}
}
// 注册回调
onSuccess(callback) {
if (this.status === 'SUCCESS') {
callback(this.data);
} else {
this.callbacks.success.push(callback);
}
}
onError(callback) {
if (this.status === 'ERROR') {
callback(this.error);
} else {
this.callbacks.error.push(callback);
}
}
}
4.3 请求队列管理器
现在,我们有了一个能跟踪状态的对象,接下来需要一个“调度员”来管理多个请求:
class RequestQueue {
constructor(maxConcurrency = 5) {
this.queue = []; // 待处理队列
this.activeRequests = new Map(); // 当前正在进行的请求
this.completedRequests = new Map(); // 已完成的请求
this.maxConcurrency = maxConcurrency;
}
// 添加请求到队列
add(requestId, config, priority = 0) {
const requestState = new RequestState(requestId, config);
requestState.priority = priority;
// 检查是否已经存在相同请求
if (this.activeRequests.has(requestId) || this.completedRequests.has(requestId)) {
return requestState;
}
this.queue.push(requestState);
this.queue.sort((a, b) => b.priority - a.priority); // 按优先级排序
this.processQueue();
return requestState;
}
// 处理队列
processQueue() {
// 如果当前请求数未达到上限,且队列不为空,则继续发送
while (this.activeRequests.size < this.maxConcurrency && this.queue.length > 0) {
const request = this.queue.shift();
this.executeRequest(request);
}
}
// 执行单个请求
executeRequest(requestState) {
this.activeRequests.set(requestState.requestId, requestState);
requestState.setSent();
// 发起实际请求
$.ajax({
url: requestState.config.url,
method: requestState.config.method || 'GET',
data: requestState.config.data,
headers: {
'X-Request-Id': requestState.requestId,
...requestState.config.headers
},
success: (data) => {
requestState.resolve(data);
this.activeRequests.delete(requestState.requestId);
this.completedRequests.set(requestState.requestId, requestState);
this.processQueue(); // 继续处理队列
},
error: (xhr, status, error) => {
requestState.reject(new Error(error));
this.activeRequests.delete(requestState.requestId);
this.completedRequests.set(requestState.requestId, requestState);
this.processQueue(); // 继续处理队列
}
});
}
// 取消请求
cancel(requestId) {
const request = this.activeRequests.get(requestId) || this.queue.find(r => r.requestId === requestId);
if (request) {
request.cancel();
if (this.activeRequests.has(requestId)) {
this.activeRequests.delete(requestId);
// 这里需要实际取消AJAX请求,可以用$.ajax的abort方法
}
}
}
// 获取请求状态
getStatus(requestId) {
return this.activeRequests.get(requestId) || this.completedRequests.get(requestId);
}
}
避免回调地狱:Promise与async/await
有了状态管理和队列控制,我们终于可以优雅地处理并发请求了。
5.1 并发执行多个请求
// 使用Promise.all并发执行
async function loadDashboardData() {
const queue = new RequestQueue(3); // 最多3个并发
// 定义多个请求
const userPromise = queue.add('user', { url: '/api/user', priority: 1 })
.onSuccess(data => data);
const orderPromise = queue.add('order', { url: '/api/order', priority: 2 })
.onSuccess(data => data);
const productPromise = queue.add('product', { url: '/api/product', priority: 3 })
.onSuccess(data => data);
try {
// 等待所有请求完成
const [user, order, product] = await Promise.all([userPromise, orderPromise, productPromise]);
// 渲染页面
renderDashboard({ user, order, product });
} catch (error) {
console.error('加载数据失败:', error);
showError('数据加载失败,请重试');
}
}
5.2 串行执行(按依赖顺序)
// 使用async/await串行执行
async function loadUserProfile() {
const queue = new RequestQueue(1); // 串行执行
// 先获取用户基本信息
const user = await queue.add('user', { url: '/api/user', priority: 1 })
.onSuccess(data => data);
// 再获取用户的订单(依赖用户ID)
const orders = await queue.add('orders', {
url: `/api/orders/${user.id}`,
priority: 2
}).onSuccess(data => data);
// 最后获取用户推荐(依赖用户和订单信息)
const recommendations = await queue.add('recommendations', {
url: `/api/recommendations?userId=${user.id}&orderCount=${orders.length}`,
priority: 3
}).onSuccess(data => data);
renderProfile(user, orders, recommendations);
}
请求队列优先级排序
在实际应用中,不同的请求有不同的优先级。比如:
- 高优先级:用户信息、核心数据
- 中优先级:推荐内容、评论列表
- 低优先级:广告、统计信息
6.1 优先级队列实现
class PriorityRequestQueue extends RequestQueue {
constructor(maxConcurrency = 5) {
super(maxConcurrency);
this.priorityLevels = {
HIGH: 100,
MEDIUM: 50,
LOW: 10
};
}
addHighPriority(requestId, config) {
return this.add(requestId, config, this.priorityLevels.HIGH);
}
addMediumPriority(requestId, config) {
return this.add(requestId, config, this.priorityLevels.MEDIUM);
}
addLowPriority(requestId, config) {
return this.add(requestId, config, this.priorityLevels.LOW);
}
}
// 使用示例
async function loadPage() {
const queue = new PriorityRequestQueue(3);
// 高优先级:先加载用户信息
const user = await queue.addHighPriority('user', {
url: '/api/user'
}).onSuccess(data => data);
// 中优先级:加载订单列表
const orders = await queue.addMediumPriority('orders', {
url: `/api/orders?userId=${user.id}`
}).onSuccess(data => data);
// 低优先级:后台加载广告
queue.addLowPriority('ads', {
url: '/api/ads'
}).onSuccess(data => {
renderAds(data);
});
// 主流程等待高、中优先级
renderMainContent(user, orders);
}
错误重试机制
网络请求不可能永远成功,我们需要一个健壮的失败处理机制。
7.1 简单重试
class RetryableRequest {
constructor(maxRetries = 3, delay = 1000) {
this.maxRetries = maxRetries;
this.delay = delay;
}
async execute(requestFn) {
let lastError;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const result = await requestFn();
return result;
} catch (error) {
lastError = error;
console.warn(`尝试 ${attempt}/${this.maxRetries} 失败:`, error.message);
if (attempt < this.maxRetries) {
await this.sleep(this.delay * attempt); // 指数退避
}
}
}
throw lastError;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// 使用示例
const retry = new RetryableRequest(3, 1000);
async function fetchUserData(userId) {
return retry.execute(async () => {
const response = await fetch(`/api/user/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
}
7.2 智能重试策略
更高级的重试策略会根据错误类型决定是否需要重试:
”`javascript class SmartRetryRequest extends RetryableRequest { constructor(maxRetries = 3, delay = 1000) {
super(maxRetries, delay);
this.retryableStatusCodes = [408, 429, 500, 502, 503, 504];
this.retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'NETWORK_ERROR'];
}
shouldRetry(error, attempt) {
// 客户端错误(4xx)通常不需要重试
if (error.status && error.status < 500 && error.status !== 408 && error.status !== 429) {
return false;
}
// 检查错误类型
if (error.code && this.retryableErrors.includes(error.code)) {
return true;
}
// 检查HTTP状态码
if (error.status && this.retryableStatusCodes.includes(error.status)) {
return attempt < this.maxRetries;
}
return false;
}
async execute(requestFn) {
let lastError;
