早上好,中午好,晚上好
近日志哥在对公司内部系统进行性能优化时,当表单存在多个用户选择控件时,每个控件的options数据都会请求相同的接口,只是有些参数不一样,考虑能否进行接口缓存,减轻服务端压力。
JavaScript原生实现加锁
第一版:
- 创建一个简单的缓存对象来存储基于参数的响应
- 当缓存对象中存在基于参数的缓存时,返回对应缓存数据
- 否则参数作为key,缓存到缓存对象上
代码实现:
const cache = {};
function generateCacheKey(params) {
return JSON.stringify(params);
}
function fetchData(url, params) {
const cacheKey = generateCacheKey(params);
// 检查缓存中是否已有该请求的响应
if (cache[cacheKey]) {
return Promise.resolve(cache[cacheKey]); // 返回缓存的响应
}
// 如果缓存中没有,则发起请求
return fetch(`${url}?${new URLSearchParams(params)}`)
.then(data => {
cache[cacheKey] = data; // 存储返回的数据到缓存对象上
return data;
});
}
上面的方案有不足之处,就是当一个页面很多地方同时调用这个接口的时候,接口数据还没回来,也就是未能保存到cache上,那么还是会发起网络请求。
第二版:处理并发请求
为了避免对同一个参数组合的重复请求,添加一个锁机制来确保同一时间只有一个请求被发送。
那这个锁该怎么设计呢?
我们可以这样,当相同的参数的时候,返回相同的Promise实例对象,也就是:
const locks = {
[cacheKey1]: Promise,
[cacheKey2]: Promise2,
...
}
然后在订阅数据完成后,释放锁。
delete locks[cacheKey];
代码实现:
const cache = {};
const locks = {};
function generateCacheKey(params) {
return JSON.stringify(params);
}
function fetchData(url, params) {
const cacheKey = generateCacheKey(params);
// 检查缓存中是否已有该请求的响应
if (cache[cacheKey]) {
return Promise.resolve(cache[cacheKey]); // 返回缓存的响应
}
// 如果缓存中没有,则发起请求
return fetch(`${url}?${new URLSearchParams(params)}`)
.then(data => {
cache[cacheKey] = data; // 存储返回的数据到缓存对象上
return data;
});
}
function fetchDataWithLock(url, params) {
const cacheKey = generateCacheKey(params);
// 如果已有锁,则返回同一个Promise
if (locks[cacheKey]) {
return locks[cacheKey];
}
// 设置锁
locks[cacheKey] = fetchData(url, params).finally(() => {
// 请求完成后释放锁
delete locks[cacheKey];
});
return locks[cacheKey];
}
使用:
const api = '/data/xxx';
fetchDataWithLock(api, { query: 'zhige', page: 1 })
.then(data => console.log(data))
.catch(error => console.error(error));
上面的代码其实已经可以实现并发请求缓存了,但还是不好用:
- 缓存失效:未实现缓存的过期时间
- 内存泄漏:何时清理缓存和锁,特别是当它们不再需要时
- 错误处理: 如果接口出错还是会返回相同的Promise,也就是未考虑异常情况
那如果考虑上述情况,那这个封装就一大坨了。
那有没有简单的方式呢?
使用Rxjs的ReplaySubject
志哥因公司的技术栈为Angular,所以接触了Angular的生态,在Angular的生态里,官方高度使用Rxjs,相当于使用Angular技术,就必须学习使用Rxjs。Rxjs里提供了一个类ReplaySubject。用这个类来实现加锁,将会相当轻松,原来几十上百行封装的代码,使用ReplaySubject只用区区10行以内的代码就可以实现,且帮你考虑了很多边界情况。
上代码:
@Injectable({
providedIn: 'root',
})
export class getUesrsHttpService {
private subjects = new Map<string, ReplaySubject<any>>();
fetchData(url, params) {
const cacheKey = generateCacheKey(params);
if (!subjects.has(cacheKey)) {
const subject = new ReplaySubject<any>(1);
subjects.set(cacheKey, subject);
this.http.post(url, params).pipe(map<any, any>((res) => res.result)).subscribe(subject);
}
return this.subjects.get(cacheKey).asObservable();
}
}
使用:
const api = '/data/xxx';
fetchData(api, { query: 'zhige', page: 1 }).subscribe(data => {
console.log(data)
});
上面使用ReplaySubject的例子中,无论有多少个组件订阅fetchData()
方法,只要参数相同,都只会有一个HTTP请求被发出,并且所有订阅者都会接收到相同的数据。
志哥我想说
如果想学习Rxjs,可以看志哥的另一篇文章:juejin.cn/post/736607…
想详细了解ReplaySubject用法,点击:cn.rx.js.org/manual/over…
如果惊叹Rxjs的强大,那么你完全有潜力成为团队中的推广大使,引领大家深入了解并广泛应用这一技术。