邂逅quick-lru,如何实现一个基于 lru 算法的数据结构❓

hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~

在看这个库的源码之前,先来了解一下什么是lru算法。

lru算法是一种内存数据淘汰策略,使用常见是当内存不足时,需要淘汰最近最少使用的数据。

例如我们演示一下一个简单的lru算法工作过程,使用字母来代表任务的执行频率情况:

一开始我们有一块长度为 3 的空间:

  • 初始状态
  1. 增加 A 元素
  1. 增加 B 元素
  1. 增加 C 元素

  1. 增加 D 元素,此时整个空间已满,A 元素被删除
  1. 增加 B 元素,但是由于此时空间内部已经存在 B 元素,所以直接将 B 元素变为活跃状态

一. 基本用法

安装

js 复制代码
npm install quick-lru

基本使用。下面是一个官方例子中一个简单的存取值的使用方法。set​为设置值的操作,has​判断内容是否已存在。get​获取值。

js 复制代码
import QuickLRU from 'quick-lru';

const lru = new QuickLRU({maxSize: 1000});

lru.set('🦄', '🌈');

lru.has('🦄');
//=> true

lru.get('🦄');
//=> '🌈'

二. 整体结构

quick-lru​继承了Map创建了一个类,继承Map是为了更方面的使用其原型链上的方法,避免需要重新手动实现Map中的方法。用来进行关于此数据结构的一切相关操作。包含constructor​构造器,私有方法。实例方法等。

由于在源码中存在很多由实例方法来调用的私有方法,所以我们只关注QuickLRU​类暴漏出来的实例方法。

js 复制代码
export default class QuickLRU extends Map {

	constructor(options = {}) {}
	// 缓存容器,提高切换效率
	get __oldCache() {}

	#emitEvictions(cache) {}

	#deleteIfExpired(key, item) {}

	#getOrDeleteIfExpired(key, item) {}

	#getItemValue(key, item) {}

	#peek(key, cache) {}
	// 增加
	#set(key, value) {}

	#moveToRecent(key, item) {
		this.#oldCache.delete(key);
		this.#set(key, item);
	}

	* #entriesAscending() {}
	// 获取值
	get(key) {}
	// 设置值
	set(key, value, {maxAge = this.#maxAge} = {}) {}
	// 判断值是否存在
	has(key) {}

	peek(key) {}
	// 删除
	delete(key) {}
	// 清空
	clear() {}
	// 调整缓存大小
	resize(newSize) {}

	* keys() {}

	* values() {}

	* [Symbol.iterator]() {}

	* entriesDescending() {}

	* entriesAscending() {}
	// 获取缓存大小
	get size() {}
	// 获取最大容量
	get maxSize() {}
	// 排序
	entries() {}

	forEach(callbackFunction, thisArgument = this) {}

	get [Symbol.toStringTag]() {}
}

constructor

js 复制代码
export default class QuickLRU extends Map {
	// 长度
	#size = 0;
	// 缓存容器,存在两个容器
	#cache = new Map();
	#oldCache = new Map();
	#maxSize;
	#maxAge;
	#onEviction;
	constructor(options = {}) {
		super();

		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');
		}
		// 初始化maxSize,最大容量
		this.#maxSize = options.maxSize;
		// 初始化最大时长 maxAge
		this.#maxAge = options.maxAge || Number.POSITIVE_INFINITY;
		// 自定义事件,用户传入的自定义事件,作为函数在每个缓存值中执行
		this.#onEviction = options.onEviction;
	}
}

初始化操作,默认的options​参数是空对象。其中maxSize​代表最大容量,判断maxSize​是否合法,如果没有传或者小于 0 报错。

判断是否定义了最大时长 maxAge​,如果定义了但是但是等于 0 报错。

支持onEviction​配置自定义事件,用户传入的自定义事件,作为函数在每个缓存值中执行。

最后初始化onEviction​属性。

  • 为什么使用了两个Map()来实现?

目的还是为了提高访问效率。 ‍

cache​用来放置最近被设置和访问缓存中的值;当cache​中值的长度超过最大储存限制maxSize​时,cache​中的全部值都移动到oldCache​中,然后清空cache​。获取元素时优先从cache​中查找,然后在oldCache​查找。如果在oldCache​中找到需要把元素移入到cache​中。 ‍

set

  • 首先判断用户设置的过期日期,判断是否为合法日期(是否为number类型并且不是无穷大)
  • 如果过期时间为合法值则将到期日期设置为当前时间加上用户传入的maxAge
  • 判断cache中是否已经存在元素?如果元素已经存在进行更新当前的值和过期时间,否则调用#set设置新增元素
js 复制代码
set(key, value, {maxAge = this.#maxAge} = {}) {
	const expiry = typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY
		? (Date.now() + maxAge)
		: undefined;
	// 是否已经存在?
	if (this.#cache.has(key)) {
		this.#cache.set(key, {
			value,
			expiry,
		});
	} else {
		// 不存在,调用set进行新增操作
		this.#set(key, {value, expiry});
	}

	return this;
}

#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();
	}
}
// 遍历所有的值,执行用户传入的回调函数
#emitEvictions(cache) {
	if (typeof this.#onEviction !== 'function') {
		return;
	}

	for (const [key, item] of cache) {
		this.#onEviction(key, item.value);
	}
}

has

has​方法首先会在cache​中查找,然后在oldCache​中查找。

在内部逻辑deleteIfExpired​中检查有没有过期?

在检查只时还会验证值是否过期。检查过期时间,如果过期时间为数字并小于等于当前时间说明元素已经过期可以删除;检查用户是否传onEviction​函数,如果传了则调用;最后调用delete()​方法删除元素。

js 复制代码
has(key) {
	// 在cache中查找
	if (this.#cache.has(key)) {
		return !this.#deleteIfExpired(key, this.#cache.get(key));
	}
	// 在oldCache中查找
	if (this.#oldCache.has(key)) {
		return !this.#deleteIfExpired(key, this.#oldCache.get(key));
	}

	return false;
}

#deleteIfExpired(key, item) {
	// 检查过期时间
	if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
		// 是否传递了onEviction函数?
		if (typeof this.#onEviction === 'function') {
			this.#onEviction(key, item.value);
		}

		return this.delete(key);
	}

	return false;
}

delete

删除操作同样先在cache​中删除,然后在oldCache​删除,删除后size​减一。

js 复制代码
delete(key) {
	const deleted = this.#cache.delete(key);
	if (deleted) {
		this.#size--;
	}

	return this.#oldCache.delete(key) || deleted;
}

get

  • 首先在cache中查找值,然后根据过期时间判断该值是否已经过期,如果过期将会被删除,没过期返回该值
  • oldCache中查找,如果查找到并且没有过期,那么将该key对应的值重新设置(变为活跃状态),具体操作是:先将该值删除,然后重新调用set方法添加。
js 复制代码
get(key) {
	// cache查找
	if (this.#cache.has(key)) {
		const item = this.#cache.get(key);
		return this.#getItemValue(key, item);
	}
	// oldCache查找
	if (this.#oldCache.has(key)) {
		const item = this.#oldCache.get(key);
		// 判断是否过期
		if (this.#deleteIfExpired(key, item) === false) {
			// 移动至cache中
			this.#moveToRecent(key, item);
			return item.value;
		}
	}
}

// 返回值,会进行是否过期的验证
#getItemValue(key, item) {
	return item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value;
}

// 判断是否过期,过期会被删除
#getOrDeleteIfExpired(key, item) {
	const deleted = this.#deleteIfExpired(key, item);
	if (deleted === false) {
		return item.value;
	}
}

// 变为活跃状态
// 先从oldCache中删除
// 然后重新设置值到cache中
#moveToRecent(key, item) {
	this.#oldCache.delete(key);
	this.#set(key, item);
}

peek

获取元素。但是与get​的不同之处是:如果查找到值在oldCache​中,不标记为最近使用,也就是不会移动到cache​中。

js 复制代码
peek(key) {
	// 同样是在cache中查找
	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);
}

clear

清空

js 复制代码
clear() {
	this.#cache.clear();
	this.#oldCache.clear();
	this.#size = 0;
}

size

首先size​属性代表cache​的长度,如果size​为false,则说明cache​为空,直接返回oldCache​的长度。

最后判断oldCache​中的元素是否存在于在cache​中,如果不存在则说明这些元素不在cache​中,但是最后也要算到最终的size​结果中。最后返回这个总和与maxSize​二者之间的最小值。

js 复制代码
get size() {
	// size是否为空?
	if (!this.#size) {
		return this.#oldCache.size;
	}

	let oldCacheSize = 0;
	// 遍历oldCache所有的key,查找是否存在于cache中
	for (const key of this.#oldCache.keys()) {
		if (!this.#cache.has(key)) {
			oldCacheSize++;
		}
	}

	return Math.min(this.#size + oldCacheSize, this.#maxSize);
}

resize

更改长度。

  • 计算已存在的集合长度,包含cache​和oldCache

  • 如果没超过新的长度

    • 构建新的cache
  • 如果超过新的长度

    • 根据新的长度截取
    • 利用剪切后的集合构建新的oldCache
js 复制代码
resize(newSize) {
	if (!(newSize && newSize > 0)) {
		throw new TypeError('`maxSize` must be a number greater than 0');
	}
	// 获取所有缓存的2值,包含cache和oldCache
	const items = [...this.#entriesAscending()];
	// 超出的长度
	const removeCount = items.length - newSize;
	// 如果没超过新的长度
	if (removeCount < 0) {
		// 利用items构建新的cache
		this.#cache = new Map(items);
		this.#oldCache = new Map();
		this.#size = items.length;
	} else {
		// 如果超过新的长度了
		// 删除所有超出的长度并对其执行onEviction函数
		if (removeCount > 0) {
			this.#emitEvictions(items.slice(0, removeCount));
		}
		// 利用剪切后的items构建新的oldCache
		this.#oldCache = new Map(items.slice(removeCount));
		this.#cache = new Map();
		this.#size = 0;
	}

	this.#maxSize = newSize;
}

一个生成器函数,首先返回oldCache​中没过期的值,最后返回cache​中没过期的值。

js 复制代码
* #entriesAscending() {
	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 item;
			}
		}
	}

	for (const item of this.#cache) {
		const [key, value] = item;
		const deleted = this.#deleteIfExpired(key, value);
		if (deleted === false) {
			yield item;
		}
	}
}

[Symbol.iterator]

迭代器,作用是,当使用for of​等方式来遍历QuickLRU​时,每次遍历到的值返回的结果会按照yield​返回的结果。在[Symbol.iterator]​遍历器中会依次遍历cache​和oldCache​两个集合的内容。其中cache​中迭代值的依据是是否过期。oldCache​迭代值的依据是不在cache​中并且没有过期的值。

js 复制代码
* [Symbol.iterator]() {
	// 遍历cache
	for (const item of this.#cache) {
		const [key, value] = item;
		const deleted = this.#deleteIfExpired(key, value);
		// 判断是否过期
		if (deleted === false) {
			yield [key, value.value];
		}
	}
	// 遍历oldCache
	for (const item of this.#oldCache) {
		const [key, value] = item;
		// 是否存在于cache中?
		if (!this.#cache.has(key)) {
			// 是否已过期?
			const deleted = this.#deleteIfExpired(key, value);
			if (deleted === false) {
				yield [key, value.value];
			}
		}
	}
}

番外:Iterator(遍历器)的概念

以下来自阮一峰es6文档,详情戳 👉 Iterator 和 for...of 循环

JavaScript 原有的表示"集合"的数据结构,主要是数组(Array​)和对象(Object​),ES6 又添加了Map​和Set​。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map​,Map​的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of​循环,Iterator 接口主要供for...of​消费。

js 复制代码
const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
      }
    };
  }
};

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next​方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next​方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next​方法,直到它指向数据结构的结束位置。

每一次调用next​方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value​和done​两个属性的对象。其中,value​属性是当前成员的值,done​属性是一个布尔值,表示遍历是否结束。

其中在QuickLRU​中定义的yield​关键字与next​方法的作用类似。

‍ ‍写在最后

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳 ‍

相关推荐
Jayson柴4 分钟前
高德地图key
前端·javascript
vener_4 分钟前
ruoyi-python 若依python版本部署及新增模块
前端·javascript·vue.js·ruoyi
发奋图强强强18 分钟前
10/1 力扣 49.字母异位词分组
算法·leetcode·职场和发展
fly丶知秋21 分钟前
利用 Page Visibility API 优化网页性能与用户体验
javascript·pagevisibility
谢尔登28 分钟前
【Webpack】优化前端开发环境的热更新效率
前端·webpack·node.js
howard200533 分钟前
初试Bootstrap前端框架
前端·bootstrap
yi碗汤园1 小时前
【一文读懂】C#如何实现通用的排序功能
开发语言·数据结构·算法·c#
Mr.LiLong1 小时前
css 边框流光效果
前端·css
AutoAutoJack1 小时前
C# 字符串(String)的应用说明一
开发语言·数据结构·算法·架构·c#
0wioiw01 小时前
Vue+Flask
前端·vue.js·flask