前端使用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 方法用于展示浏览历史记录。

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

相关推荐
red润2 分钟前
使用 HTML5 Canvas 实现动态蜈蚣动画
前端·html·html5
sg_knight9 分钟前
VSCode如何修改默认扩展路径和用户文件夹目录到D盘
前端·ide·vscode·编辑器·web
一个处女座的程序猿O(∩_∩)O19 分钟前
完成第一个 Vue3.2 项目后,这是我的技术总结
前端·vue.js
mubeibeinv19 分钟前
项目搭建+图片(添加+图片)
java·服务器·前端
逆旅行天涯26 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
m0_748255261 小时前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
长风清留扬1 小时前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构