从Vue的keep-alive看LRU的精妙设计
在Vue的源码中,keep-alive组件正是使用LRU(Least Recently Used)算法来管理缓存组件的典型范例。让我们先看其核心实现片段:
js
// Vue源码中的keep-alive部分实现
export default {
name: 'keep-alive',
abstract: true,
props: {
max: [String, Number] // 最大缓存数
},
created() {
this.cache = Object.create(null) // 缓存对象
this.keys = [] // 缓存键数组(维护访问顺序)
},
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
if (vnode) {
const key = vnode.key ?? getComponentKey(vnode)
if (this.cache[key]) {
// 命中缓存时调整key顺序
vnode.componentInstance = this.cache[key].componentInstance
remove(this.keys, key)
this.keys.push(key) // 移到数组末尾表示最近使用
} else {
this.cache[key] = vnode
this.keys.push(key)
// 超出限制时删除最久未使用的
if (this.max && this.keys.length > parseInt(this.max)) {
pruneCacheEntry(this.cache, this.keys[0], this.keys)
}
}
}
return vnode
}
}
这段代码揭示了LRU算法的核心思想:通过维护使用顺序来智能淘汰最久未使用的缓存项。这种设计完美解决了有限缓存空间的高效利用问题。
接口缓存中的LRU实践
在我们的接口缓存场景中,同样面临着内存管理的挑战。升级后的memoReq
函数通过首先封装一下LRUCache
类,它实现了专业级的缓存管理(多个项目使用):
js
class LRUCache<T> {
private capacity: number;
private cache = new Map<string, T>();
constructor(capacity: number) {
this.capacity = capacity;
}
get(key: string): T | undefined {
if (this.cache.has(key)) {
// 移到末尾(最近使用)
const value = this.cache.get(key)!;
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
return undefined;
}
set(key: string, value: T): void {
if (this.cache.has(key)) {
// 更新现有项
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的项(第一个)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
has(key: string): boolean {
return this.cache.has(key);
}
delete(key: string): boolean {
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
getSize(): number {
return this.cache.size;
}
}
性能优化双保险:LRU + 时效性控制
升级版的memoReq
将LRU算法与时间过期策略相结合,形成了双重保护机制:
js
/**
* 缓存请求
*
* @param fn 不要为箭头函数
* @param cacheTimeMS 缓存时间,单位毫秒;若为 0 则不缓存, 只解决并发请求的情况;
*/
export function memoReq<T, D>(fn: (...args: any[]) => Promise<AxiosResponse<T, D>>, cacheTimeMS = 0) {
return function (...args: any[]) {
console.log(pendingRequests.getSize());
// 生成缓存key,基于函数名和参数
const key = generateCacheKey(fn, args);
// 是否有缓存
if (pendingRequests.has(key)) {
const cache = pendingRequests.get(key)!;
// 没过期
if (Date.now() - cache.responseTimeMs < cache.cacheTimeMS) {
return cache.promise;
}
pendingRequests.delete(key); // 过期就删除缓存
}
// 没缓存
const promise = fn(...args).finally(() => {
if (cacheTimeMS === 0) {
pendingRequests.delete(key);
return;
}
const prev = pendingRequests.get(key);
if (prev) {
pendingRequests.set(key, {
...prev,
responseTimeMs: Date.now(),
});
}
});
pendingRequests.set(key, {
promise,
cacheTimeMS,
responseTimeMs: Infinity,
});
return promise;
};
}
生成短key-防止参数过长
js
/**
* 生成请求参数的哈希值作为缓存键(浏览器兼容版本)
* @param fn 函数名
* @param args 参数列表
* @returns 哈希字符串
*/
function generateCacheKey(fn: Function, args: any[]): string {
// 在浏览器环境中使用简单但更可靠的哈希算法
const str =
fn.name +
JSON.stringify(args, (key, value) => {
// 对于对象,进行排序以确保相同内容产生相同字符串
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return Object.keys(value)
.sort()
.reduce((sorted: any, key) => {
sorted[key] = value[key];
return sorted;
}, {});
}
return value;
});
// 使用更强的哈希算法
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // 转换为32位整数
}
// 转换为无符号32位整数并格式化为字符串
return (hash >>> 0).toString(36);
}
可扩展方向
-
监控接口速度:
-
错误恢复机制:
架构思考延伸
正如Vue的keep-alive改变了组件生命周期管理方式,我们的LRU缓存方案重新定义了接口请求的资源管理策略。这种架构思维可以推广到任何需要资源管理的场景,体现了有限资源下的最优分配思想。