前端使用JavaScript实现一个LRU缓存

引言

LRU(Least Recently Used)算法是一种广泛应用于内存管理和缓存系统的策略,在微前端、状态管理以及性能优化等场景下,合理使用缓存机制能够有效提升应用性能。本文将介绍LRU算法的基本原理,并通过JavaScript实现案例,帮助读者理解其在前端开发中的应用场景。

LRU算法原理

LRU(最近最少使用)算法是一种常用的缓存淘汰策略,它假定"最近最久未使用的数据在未来被访问的可能性最小"。当缓存空间不足时,LRU会优先移除最近最少使用的数据,为新数据腾出存储空间。

实现方式

哈希表适合快速查找、插入和删除的场景,而双向链表适合频繁插入和删除节点的场景。在某些情况下,这两种数据结构也可以结合实现LRU缓存算法时,可以使用哈希表存储 key 和对应的节点,双向链表存储实际的数据节点,以实现快速的查找和插入删除操作。

图解示例

假设设计一个容量为3的LRU缓存

  1. 首先添加3个元素
  2. 哈希表中依次添加数据的key值:key1,key2,key3
  3. 双向链表存储哈希对(key,value),key3是最后一个添加的(最新添加记录是key3),那么key3对应的哈希值添加到链表的头部node3。表示最近使用的。

这个时候如果要添加第四个数据,key4放入哈希表,node4放入双向链表头部,node4包含key4,value4,然而此时容量不足,需要删除一个元素,从尾部删除一个最久没使用的元素,删除上面图示中的node1的数据,同时在哈希表中删除node1对应的key1

把node4添加到链表头部,key4添加进哈希表是同步进行的。

如果最近要访问key3,需要把node3从当前位置删除,并插入到链表头部

简而言之:

每次添加元素到链表中的时候都是从头部添加

每次删除元素的时候都是从尾部删除

删除的时候同时从哈希表里面删除对应的key

再次访问的元素,需要把元素移动到链表的头部

实现代码

使用哈希链表,可以在每个缓存项的节点上同时存储键值信息以及指向链表中前后节点的引用。当一个缓存项被访问时,先通过哈希表找到对应节点,然后将其从原有位置移出并插入链表尾部

复制代码
class LRUCacheNode {
  constructor(key, value) {
    this.key = key;
    this.value = value;
    this.prev = null;
    this.next = null;
  }
}

class LRUCache {
  constructor(capacity = 500) {
    this.capacity = capacity;
    this.cacheMap = new Map(); // 使用哈希表存储键值对
    this.doubleLinkedList = new DoublyLinkedList(); // 双向链表维护缓存顺序
  }

  get(key) {
    if (this.cacheMap.has(key)) {
      const node = this.cacheMap.get(key);
      this.doubleLinkedList.moveToTail(node); // 将节点移动到链表尾部,表示最新访问
      return node.value;
    }
    return -1; // 或者返回null,表示key不存在于缓存中
  }

  put(key, value) {
    if (this.cacheMap.has(key)) {
      const node = this.cacheMap.get(key);
      node.value = value;
      this.doubleLinkedList.moveToTail(node);
    } else {
      if (this.cacheMap.size >= this.capacity) {
        const headNode = this.doubleLinkedList.deleteHead();
        this.cacheMap.delete(headNode.key); // 移除最旧的缓存项
      }
      const newNode = new Node(key, value);
      this.cacheMap.set(key, newNode);
      this.doubleLinkedList.addToTail(newNode);
    }
  }
}

// 双向链表类和节点类的实现略(根据实际需求实现)
class Node {
  constructor(key, value) {
    this.key = key; // 节点键值
    this.value = value; // 节点数据值
    this.prev = null; // 前驱节点引用
    this.next = null; // 后继节点引用
  }
}
class DoublyLinkedList {
  constructor() {
    this.head = null; // 头节点
    this.tail = null; // 尾节点
  }

  /**
   * 添加节点到链表尾部
   * @param {Node} newNode 新节点
   */
  addToTail(newNode) {
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.prev = this.tail;
      this.tail.next = newNode;
      this.tail = newNode;
    }
  }

  /**
   * 移除头节点并返回
   * @returns {Node | null} 删除的头节点或null(如果链表为空)
   */
  deleteHead() {
    if (!this.head) return null;

    const deletedNode = this.head;
    this.head = this.head.next;

    if (this.head) {
      this.head.prev = null;
    } else {
      this.tail = null;
    }

    return deletedNode;
  }

  /**
   * 将指定节点移动到链表尾部
   * @param {Node} node 需要移动的节点
   */
  moveToTail(node) {
    if (node === this.tail) return; // 如果已经是尾节点,则无需移动

    // 断开当前节点与前后节点的连接
    node.prev.next = node.next;
    if (node.next) node.next.prev = node.prev;

    // 将节点添加至链表尾部
    this.addToTail(node);
  }
  
  // 其他可能的方法,如查找节点、在指定位置插入节点等...
}

使用场景

路由缓存:Vue.js 中的 keep-alive 组件虽然并未直接采用LRU算法,但在实际项目中,我们可以基于LRU策略自定义实现路由组件的缓存功能。

资源加载:对于频繁请求且响应较慢的API,可以通过LRU缓存最近请求的结果,减少网络请求次数。

状态管理:在Vuex或Redux等状态管理库中,也可以利用LRU算法进行缓存,避免频繁计算或获取昂贵的状态。

业务场景:电商大促,浏览器浏览历史,微博热点。实现的具体可能不同,但是思路均可使用LRU缓存实现.

网页浏览历史

实现一个简单的浏览历史记录功能

复制代码
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map();
    }

    get(key) {
        if (this.cache.has(key)) {
            const value = this.cache.get(key);
            // 删除旧数据再重新插入,以更新最近访问的顺序
            this.cache.delete(key);
            this.cache.set(key, value);
            return value;
        }
        return null;
    }

    put(key, value) {
        if (this.cache.has(key)) {
            this.cache.delete(key);
        } else if (this.cache.size >= this.capacity) {
            // 超出容量时删除最久未访问的数据(即最近使用频率最低的数据)
            const keys = this.cache.keys();
            this.cache.delete(keys.next().value);
        }
        this.cache.set(key, value);
    }

    displayHistory() {
        console.log("Browser History:");
        for (let [key, value] of this.cache) {
            console.log(key + " -> " + value);
        }
    }
}

// 使用示例
const historyCache = new LRUCache(5); // 设置缓存容量为5

historyCache.put("Page 1", "www.page1.com");
historyCache.put("Page 2", "www.page2.com");
historyCache.put("Page 3", "www.page3.com");
historyCache.get("Page 1");

// 输出浏览历史记录
historyCache.displayHistory();

在上面的示例中,LRUCache 类实现了一个简单的 LRU 缓存,通过 get 方法获取历史记录,并通过 put 方法添加历史记录。displayHistory 方法用于展示浏览历史记录。

可以根据实际需求进一步扩展和优化这个示例,比如添加时间戳来记录访问时间、持久化历史记录到本地存储等功能。希望这个示例能帮助你实现浏览历史记录功能。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax