浅谈LRU

78 阅读3分钟

前言

假设一个手机后台应用:我们同时打开了十几个App,但手机内存毕竟是有限的,那么系统就需要决定哪些App能保持活跃状态,哪些该被关闭以腾出空间。LRU(Least Recently Used)算法就是干这活的,例如地图10分钟前用过,那么它就会被留着;视频已经半天没碰了,就会被收起来。在Vue项目中,我们也可以通过LRU,来提升应用的流畅度。

本文将用3个真实场景,初步介绍LRU的使用方法。

一、Vue项目中的缓存痛点

先看三个典型问题:

  1. 商品列表页 -> 详情页 -> 返回列表页刷新:用户刚浏览的位置丢失,需要重新查找,体验糟糕。
  2. 复杂表单切换标签页后数据丢失:用户填写了一半的数据消失,开发人员需要处理由此引发的逻辑问题。
  3. 重复加载相同图片造成流量浪费:尤其在移动端或图片较多的场景,不必要的流量消耗直接影响成本和性能。

这些问题的核心,都是如何高效管理有限的内存资源? LRU算法正是解决以上这些问题的经典策略。

二、LRU在Vue中的实践

场景1:路由缓存优化(keep-alive 进阶版)

基础方案痛点:

<keep-alive>
  <router-view/>
</keep-alive>

此方案会缓存所有路由组件,可能导致内存占用持续增长,最终影响性能。

基于LRU的优化方案 (Vue 2 示例):


<script>
export default {
  data() {
    return {
      cacheKeys: [], // 当前缓存的组件key队列
      maxCache: 5    // 最大缓存数量
    }
  },
  render() {
    return (
      <keep-alive include={this.cacheKeys}>
        <router-view key={this.$route.fullPath} />
      </keep-alive>
    )
  },
  activated() {
    this.updateCache(this.$route)
  },
  methods: {
    updateCache(route) {
      const key = route.fullPath
      const index = this.cacheKeys.indexOf(key)

      // LRU缓存更新策略
      if (index > -1) {
        this.cacheKeys.splice(index, 1) // 移除旧位置
      } else if (this.cacheKeys.length >= this.maxCache) {
        this.cacheKeys.shift() // 移除最久未使用的
      }
      
      this.cacheKeys.push(key) // 添加到最新位置
    }
  }
}
</script>

场景2:图片懒加载 + LRU缓存

<template>
  <img :src="realSrc" v-lazy="url" /> <!-- 使用自定义指令 v-lazy -->
</template>

<script>
import { LRUCache } from './lru';

const imageCache = new LRUCache(20); // 缓存最近使用的20张图片

export default {
  directives: {
    lazy: {
      mounted(el, { value: url }) {
        const observer = new IntersectionObserver(([{ isIntersecting }]) => {
          if (isIntersecting) {
            // 图片进入视口
            if (imageCache.has(url)) {
              // 命中缓存,直接使用
              el.src = imageCache.get(url);
            } else {
              // 未命中,加载图片
              const img = new Image();
              img.src = url;
              img.onload = () => {
                // 加载成功,存入缓存
                imageCache.set(url, url);
                el.src = url;
              };
            }
            observer.unobserve(el); // 停止观察
          }
        });
        observer.observe(el); // 开始观察元素
      }
    }
  }
}
</script>

场景3:API响应缓存(Axios拦截器方案)

// utils/request.js
import LRUCache from './lru';

const apiCache = new LRUCache(50); // 缓存50个GET接口响应

export function createRequest() {
  const service = axios.create();

  // 请求拦截:检查缓存
  service.interceptors.request.use(config => {
    if (config.method === 'get' && config.cache) {
      const cacheKey = generateKey(config); // 根据请求参数生成唯一key
      if (apiCache.has(cacheKey)) {
        // 命中缓存,直接返回缓存数据,避免真实请求
        return Promise.resolve(apiCache.get(cacheKey));
      }
      config.cacheKey = cacheKey; // 标记此请求需要缓存
    }
    return config;
  });

  // 响应拦截:存储缓存
  service.interceptors.response.use(response => {
    const { config, data } = response;
    if (config.cacheKey) {
      apiCache.set(config.cacheKey, data); // 成功响应后存入缓存
    }
    return data;
  });

  return service;
}

// 使用示例
const request = createRequest();
request.get('/api/goods', { cache: true }); // 第二次调用相同请求将直接读取缓存

三、Vue专用的响应式LRU Hook

// lib/lru.js
import { reactive, toRefs } from 'vue';

export function useLRU(capacity = 10) {
  // 使用Vue的响应式系统管理状态
  const state = reactive({
    cacheMap: new Map(), // 存储键值对
    keys: []            // 存储键的顺序(最近使用->最久未用)
  });

  const get = (key) => {
    if (!state.cacheMap.has(key)) return null;

    // 更新使用顺序:将key移到数组末尾(最近使用)
    state.keys = state.keys.filter(k => k !== key);
    state.keys.push(key);

    return state.cacheMap.get(key);
  };

  const set = (key, value) => {
    if (state.cacheMap.has(key)) {
      // Key已存在:先移除旧位置
      state.keys = state.keys.filter(k => k !== key);
    } else {
      // Key不存在:检查容量
      if (state.keys.length >= capacity) {
        const oldestKey = state.keys.shift(); // 移除最久未使用的key
        state.cacheMap.delete(oldestKey);
      }
    }
    // 添加/更新键值对,并记录在keys末尾
    state.keys.push(key);
    state.cacheMap.set(key, value);
  };

  return {
    ...toRefs(state), // 转换为refs以便在模板中使用
    get,
    set
  };
}

// 在组件中使用
import { useLRU } from './lib/lru';

export default {
  setup() {
    const { cacheMap, keys, get, set } = useLRU(5);

    const getData = async (key) => {
      const cached = get(key);
      if (cached) return cached;
      // 模拟异步获取数据
      const data = await fetchData(key);
      set(key, data);
      return data;
    };

    return {
      cacheMap,
      keys,
      getData
    };
  }
}

小结

通过在Vue项目中的在路由缓存、图片懒加载和 API 响应这几个应用LRU的示例场景,咱们大致能够了解到这个算法主要是做什么的,简而言之——“让常用功能保持活跃,冷门功能适时释放