引言
LRU(Least Recently Used)算法是一种广泛应用于内存管理和缓存系统的策略,在微前端、状态管理以及性能优化等场景下,合理使用缓存机制能够有效提升应用性能。本文将介绍LRU算法的基本原理,并通过JavaScript实现案例,帮助读者理解其在前端开发中的应用场景。
LRU算法原理
LRU(最近最少使用)算法是一种常用的缓存淘汰策略,它假定“最近最久未使用的数据在未来被访问的可能性最小”。当缓存空间不足时,LRU会优先移除最近最少使用的数据,为新数据腾出存储空间。
实现方式
哈希表适合快速查找、插入和删除的场景,而双向链表适合频繁插入和删除节点的场景。在某些情况下,这两种数据结构也可以结合实现LRU缓存算法时,可以使用哈希表存储 key 和对应的节点,双向链表存储实际的数据节点,以实现快速的查找和插入删除操作。
图解示例
假设设计一个容量为3的LRU缓存
- 首先添加3个元素
- 哈希表中依次添加数据的key值:key1,key2,key3
- 双向链表存储哈希对(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); // 设置缓存容量为5historyCache.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 方法用于展示浏览历史记录。
可以根据实际需求进一步扩展和优化这个示例,比如添加时间戳来记录访问时间、持久化历史记录到本地存储等功能。希望这个示例能帮助你实现浏览历史记录功能。