持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
【若川视野 x 源码共读】第29期 | quick-lru 点击了解本期详情一起参与。
今天阅读的库是:quick-lru
什么是LRU算法
操作系统中常用的分页置换算法
- 为了提高页面命中率,发生缺页中断时,选择未使用时间最长的页面置换出去。
- 相当于一个缓存的机制
使用场景
- 在
vue中,keep-alive组件的实现
源码分析
package.json查看入口文件
测试用例
我们来看下源码怎么实现的,首先打开测试用例
// index.test-d.ts
import {expectType} from 'tsd';
import QuickLRU from './index.js';
const lru = new QuickLRU<string, number>({maxSize: 1000, maxAge: 200});
expectType<QuickLRU<string, number>>(lru.set('🦄', 1).set('🌈', 2));
expectType<number | undefined>(lru.get('🦄'));
expectType<boolean>(lru.has('🦄'));
expectType<number | undefined>(lru.peek('🦄'));
expectType<boolean>(lru.delete('🦄'));
expectType<number>(lru.size);
for (const [key, value] of lru) {
expectType<string>(key);
expectType<number>(value);
}
for (const key of lru.keys()) {
expectType<string>(key);
}
for (const value of lru.values()) {
expectType<number>(value);
}
可以看到有几个功能
- set
- get
- has
- peek
- delete
- size
- 可迭代
所列的函数与测试用例顺序相同
constructor
// index.js
constructor(options = {}) {
super();
// 判断传入参数
// 最大容量 maxSie
// 最大时长 maxAge
if (!(options.maxSize && options.maxSize > 0)) {
throw new TypeError("`maxSize` must be a number greater than 0");
}
if (typeof options.maxAge === "number" && options.maxAge === 0) {
throw new TypeError("`maxAge` must be a number greater than 0");
}
// TODO: Use private class fields when ESLint supports them.
this.maxSize = options.maxSize;
this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
this.onEviction = options.onEviction;
this.cache = new Map();
this.oldCache = new Map();
this._size = 0;
}
疑问:
quick-lru为什么使用两个Map,他们的作用是什么
set
set(key, value, { maxAge = this.maxAge } = {}) {
// 设置过期时间
const expiry =
typeof maxAge === "number" && maxAge !== Number.POSITIVE_INFINITY
? Date.now() + maxAge
: undefined;
// 如果存在key,更新当前的值和过期时间
if (this.cache.has(key)) {
this.cache.set(key, {
value,
expiry,
});
} else {
// 如果不存在,则调用_set函数设置值,size++
this._set(key, { value, expiry });
}
}
// 如果当前的key不存在
_set(key, value) {
this.cache.set(key, value);
this._size++;
// 如果当前数量超出最大限制,将cache赋值到oldCache中,并清空当前缓存
if (this._size >= this.maxSize) {
this._size = 0;
this._emitEvictions(this.oldCache);
this.oldCache = this.cache;
this.cache = new Map();
}
}
get
get(key) {
// 在cache中查找值,如果没找到,则在oldCache中继续查找
if (this.cache.has(key)) {
const item = this.cache.get(key);
return this._getItemValue(key, item);
}
if (this.oldCache.has(key)) {
// 如果在oldCache中找到,则将当前元素移到cache中
const item = this.oldCache.get(key);
if (this._deleteIfExpired(key, item) === false) {
this._moveToRecent(key, item);
return item.value;
}
}
}
has
has(key) {
if (this.cache.has(key)) {
// 过期返回 false
return !this._deleteIfExpired(key, this.cache.get(key));
}
if (this.oldCache.has(key)) {
// 过期返回 false
return !this._deleteIfExpired(key, this.oldCache.get(key));
}
return false;
}
peek
// 在cache和oldCache中查找
// 与get不同的是,如果在oldCache中找到元素,不会将元素移动到cache中
peek(key) {
if (this.cache.has(key)) {
return this._peek(key, this.cache);
}
if (this.oldCache.has(key)) {
return this._peek(key, this.oldCache);
}
}
_peek(key, cache) {
const item = cache.get(key);
return this._getItemValue(key, item);
}
delete
delete(key) {
// 删除key对应的值
const deleted = this.cache.delete(key);
if (deleted) {
this._size--;
}
return this.oldCache.delete(key) || deleted;
}
size
get size() {
if (!this._size) {
return this.oldCache.size;
}
let oldCacheSize = 0;
for (const key of this.oldCache.keys()) {
if (!this.cache.has(key)) {
oldCacheSize++;
}
}
return Math.min(this._size + oldCacheSize, this.maxSize);
}
迭代器
// 重写Symbol.iterator,实现for...of...
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator
*[Symbol.iterator]() {
for (const item of this.cache) {
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
for (const item of this.oldCache) {
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
}
}
// 排序
entries() {
return this.entriesAscending();
}
forEach(callbackFunction, thisArgument = this) {
for (const [key, value] of this.entriesAscending()) {
callbackFunction.call(thisArgument, value, key, this);
}
}
// 升序
*entriesAscending() {
for (const [key, value] of this._entriesAscending()) {
yield [key, value.value];
}
}
总结
回答上述问题
为什么使用了两个Map()来实现LRU算法,而不是使用Map+双向链表的形式?
-
查找效率更高,时间复杂度为
O(1)
这个与**HashMap+ 双向链表** 有什么区别
- 得益于
JS的Map设计,在ES6之后,Map的数据结构是有序的,即Map可以模拟出双向链表的操作
这个算法使用了两个Map来作为缓存,算法复杂度为O(1),空间换时间,效率更高。