一、真实问题场景
在实际项目中,你一定遇到过:
- SPA 首屏加载,需要同时请求 5~10 个接口甚至更多
- 用户频繁点击按钮,导致接口被重复触发
- 某个接口不稳定,导致整个页面"卡死"
- loading 状态混乱,用户体验极差
👉 这些问题本质都是:系统缺乏"请求调度能力"
二、问题本质拆解
我们把问题抽象一下:
1️⃣ 并发失控
浏览器会同时发起多个请求,容易:
- 挤爆带宽
- 阻塞关键请求
- 导致接口限流
2️⃣ 重复请求
同一个接口被多次触发:
js
getUserInfo()
getUserInfo()
getUserInfo()
👉 完全浪费资源
3️⃣ 无缓存机制
每次进入页面都重新请求:
👉 实际很多数据是可以复用的
4️⃣ 接口雪崩
某个接口挂了:
👉 页面一直重试 → 更加雪崩
三、解决思路(核心设计)
👉 一个统一模型:
text
请求 → 调度器 → 控制执行
我们需要实现:
- 队列(控制并发)
- 去重(避免重复)
- 缓存(减少请求)
- 熔断(保护系统)
- loading(统一管理)
四、队列:控制并发(核心)
为什么需要队列?
假设你同时请求10个接口:
text
❌ 同时执行 → 网络阻塞
✅ 控制为3个 → 更稳定
实现代码(升级版)
js
class RequestQueue {
constructor(max = 5) {
this.max = max;
this.running = 0;
this.queue = [];
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.run();
});
}
run() {
while (this.running < this.max && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
Promise.resolve(task())
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.run();
});
}
}
}
使用示例
js
const queue = new RequestQueue(3);
for (let i = 0; i < 10; i++) {
queue.add(() => fetch("/api")).then(console.log);
}
五、请求去重:避免重复调用
场景
用户快速点击按钮:
👉 会触发多次相同请求
解决方案
js
const pendingMap = new Map();
function requestWithDedupe(key, fn) {
if (pendingMap.has(key)) {
return pendingMap.get(key);
}
const promise = fn().finally(() => {
pendingMap.delete(key);
});
pendingMap.set(key, promise);
return promise;
}
核心思想
👉 相同 key 的请求只执行一次,其它复用 Promise
六、缓存机制:减少无效请求
场景
- 用户信息
- 配置数据
- 字典数据
实现
js
const cache = new Map();
function requestWithCache(key, fn, ttl = 5000) {
const now = Date.now();
if (cache.has(key)) {
const { data, time } = cache.get(key);
if (now - time < ttl) {
return Promise.resolve(data);
}
}
return fn().then((res) => {
cache.set(key, { data: res, time: now });
return res;
});
}
优化点
- 可加 localStorage
- 可做持久缓存
七、熔断机制:防止系统雪崩(重点)
场景
接口连续失败:
text
请求 → 失败
请求 → 失败
请求 → 失败
👉 如果继续请求,只会更糟
状态机
text
CLOSED → 正常
OPEN → 熔断
HALF → 半开(试探)
实现
js
class CircuitBreaker {
constructor(limit = 3, timeout = 5000) {
this.failCount = 0;
this.limit = limit;
this.state = "CLOSED";
this.nextTry = 0;
this.timeout = timeout;
}
async exec(fn) {
if (this.state === "OPEN") {
if (Date.now() < this.nextTry) {
return Promise.reject("熔断中");
}
this.state = "HALF";
}
try {
const res = await fn();
this.success();
return res;
} catch (e) {
this.fail();
throw e;
}
}
success() {
this.failCount = 0;
this.state = "CLOSED";
}
fail() {
this.failCount++;
if (this.failCount >= this.limit) {
this.state = "OPEN";
this.nextTry = Date.now() + this.timeout;
}
}
}
八、Loading 管理
问题
多个请求同时存在:
👉 loading 何时关闭?
解决方案
js
let loadingCount = 0;
function showLoading() {
if (loadingCount === 0) {
console.log("loading start");
}
loadingCount++;
}
function hideLoading() {
loadingCount--;
if (loadingCount === 0) {
console.log("loading end");
}
}
九、最终整合:请求调度器
js
class RequestScheduler {
constructor() {
this.queue = new RequestQueue(3);
this.breaker = new CircuitBreaker();
this.pending = new Map();
}
request(key, fn) {
if (this.pending.has(key)) {
return this.pending.get(key);
}
const task = async () => {
showLoading();
try {
return await this.breaker.exec(fn);
} finally {
hideLoading();
}
};
const p = this.queue.add(task).finally(() => {
this.pending.delete(key);
});
this.pending.set(key, p);
return p;
}
}