LRU算法在前端性能优化中的实践艺术(缓存请求函数为例)

从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);
}

可扩展方向

  1. 监控接口速度

  2. 错误恢复机制

架构思考延伸

正如Vue的keep-alive改变了组件生命周期管理方式,我们的LRU缓存方案重新定义了接口请求的资源管理策略。这种架构思维可以推广到任何需要资源管理的场景,体现了有限资源下的最优分配思想。

相关推荐
Skylar_.27 分钟前
嵌入式 - 数据结构:哈希表和排序与查找算法
数据结构·算法·嵌入式·哈希算法·散列表
破刺不会编程1 小时前
linux信号量和日志
java·linux·运维·前端·算法
汪子熙2 小时前
浏览器环境中 window.eval(vOnInit); // csp-ignore-legacy-api 的技术解析与实践意义
前端·javascript
黑色的山岗在沉睡2 小时前
【无标题】
数据结构·c++·算法·图论
BUG收容所所长2 小时前
🤖 零基础构建本地AI对话机器人:Ollama+React实战指南
前端·javascript·llm
小高0072 小时前
🚀前端异步编程:Promise vs Async/Await,实战对比与应用
前端·javascript·面试
Spider_Man2 小时前
"压"你没商量:性能优化的隐藏彩蛋
javascript·性能优化·node.js
用户87612829073742 小时前
对于通用组件如何获取表单输入,区分表单类型的试验
前端·javascript
Bdygsl3 小时前
前端开发:JavaScript(6)—— 对象
开发语言·javascript·ecmascript
2301_785038183 小时前
c++初学day1(类比C语言进行举例,具体原理等到学到更深层的东西再进行解析)
c语言·c++·算法