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

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

相关推荐
永日456704 分钟前
学习日记-HTML-day51-9.9
前端·学习·html
狗头大军之江苏分军21 分钟前
iPhone 17 vs iPhone 17 Pro:到底差在哪?买前别被忽悠了
前端
小林coding21 分钟前
再也不怕面试了!程序员 AI 面试练习神器终于上线了
前端·后端·面试
文心快码BaiduComate33 分钟前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger33 分钟前
python 通过selenium调用chrome浏览器
前端·chrome
passer98140 分钟前
基于webpack的场景解决
前端·webpack
华科云商xiao徐1 小时前
Java并发编程常见“坑”与填坑指南
javascript·数据库·爬虫
奶昔不会射手1 小时前
css3之grid布局
前端·css·css3
举个栗子dhy1 小时前
解决在父元素上同时使用 onMouseEnter和 onMouseLeave时导致下拉菜单无法正常展开或者提前收起问题
前端·javascript·react.js