【若川视野 x 源码共读】第29期 | quick-lru

217 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

quick-lru

这是个用map对象来进行lru的模拟,不同于链表,用两个cache进行存储,有点奇异,简单理解下。

链表的参考 lru-cache www.yuque.com/ruochuan12/…

参考

www.yuque.com/ruochuan12/…

www.yuque.com/ruochuan12/…

leedcode刷题理解

leetcode.cn/problems/lr…

// 正统应该是链表作法,我这取巧用的map,
// 链表作法: https://leetcode.cn/problems/lru-cache/solution/dai-ma-jian-ji-yi-chong-huan-bu-cuo-de-j-n7rj/
var LRUCache = function(capacity) {
    this.capacity = capacity
    this.map = new Map()
    //map 对象 存键是按顺序存的,如果覆盖的话不会改顺序,需要删掉原键,新增。删掉后map对象大小会自动调整。
    //删掉第一个键 可以通过map.keys().next().value得到。
};


LRUCache.prototype.get = function(key) {
    let map = this.map
    let have = map.has(key)
    if(have){
        //如果有 需要调整顺序,删掉 重新添加
        let value = map.get(key)
        map.delete(key)
        map.set(key,value)
        return value
    }
    return -1
};


LRUCache.prototype.put = function(key, value) {
    // 如果满了 但可能加的是重复的,所以可以先不减,先判断是否重复
    let map = this.map
    let have = map.has(key)
    // if(have){
    //     map.delete(key)
    // }else{
    //     if(this.capacity == map.size()){
    //         map.delete(map.keys().next().value)
    //     }
    // }
    if(have){
        map.delete(key)
    }
    if(this.capacity == map.size){
        map.delete(map.keys().next().value)
    }
    map.set(key,value)
};

api

api:www.yuque.com/ruochuan12/…

源码

github1s.com/sindresorhu…

export default class QuickLRU extends Map {
    //继承map的方法
	constructor(options = {}) {
		super();
	// 传入的最大空间要大于0且存在
		if (!(options.maxSize && options.maxSize > 0)) {
			throw new TypeError('`maxSize` must be a number greater than 0');
		}
	// 最长存储时间要是数字类型且非0,额,不是应该大于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;
        // Number.POSITIVE_INFINITY 无穷大
		this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
        // 数据清除回调
		this.onEviction = options.onEviction;
        // 两个cache 用来存数据
		this.cache = new Map();
		this.oldCache = new Map();
        // 当前容量
		this._size = 0;
	}
    // TODO: Use private class methods when targeting Node.js 16.
    //用来将要清除的数据 进行 数据清除回调
	_emitEvictions(cache) {
		if (typeof this.onEviction !== 'function') {
			return;
		}

		for (const [key, item] of cache) {
			this.onEviction(key, item.value);
		}
	}
	// 校验数据是否过期,过期去执行 数据清除回调 并删掉这个数据
	_deleteIfExpired(key, item) {
		if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
			if (typeof this.onEviction === 'function') {
				this.onEviction(key, item.value);
			}

			return this.delete(key);
		}

		return false;
	}
	// 拿数据,如果过期就删了
	_getOrDeleteIfExpired(key, item) {
		const deleted = this._deleteIfExpired(key, item);
		if (deleted === false) {
			return item.value;
		}
	}
	
	_getItemValue(key, item) {
		return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
	}

	_peek(key, cache) {
		const item = cache.get(key);

		return this._getItemValue(key, item);
	}

	_set(key, value) {
		this.cache.set(key, value);
		this._size++;

		if (this._size >= this.maxSize) {
			this._size = 0;
			this._emitEvictions(this.oldCache);
			this.oldCache = this.cache;
			this.cache = new Map();
		}
	}

	_moveToRecent(key, item) {
		this.oldCache.delete(key);
		this._set(key, item);
	}

	* _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;
			}
		}
	}
	//取值方法
 
	get(key) {
           //  cache如果有,拿出来,
		if (this.cache.has(key)) {
			const item = this.cache.get(key);
		// 如果过期了,删掉数据,啥也没返回;没过期,给值
			return this._getItemValue(key, item);
		}
		// oldCache如果有,如果过期了,删掉数据,啥也没返回;没过期,给值,还要把值删掉,加到cache里
		if (this.oldCache.has(key)) {
			const item = this.oldCache.get(key);
			if (this._deleteIfExpired(key, item) === false) {
				this._moveToRecent(key, item);
				return item.value;
			}
		}
	}
	// 存值方法,存的时候算出过期时间一起存进去,如果是重复数据,覆盖即可。
    // 如果是新数据,走_set,判断加入后size是否满了,满了就把oldCache数据进行数据清除回调,把cache给oldCache,size和cache重置,这作法我感觉怪怪的。
	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 {
			this._set(key, {value, expiry});
		}
	}
	// 判断,过期了就要删掉
	has(key) {
		if (this.cache.has(key)) {
			return !this._deleteIfExpired(key, this.cache.get(key));
		}

		if (this.oldCache.has(key)) {
			return !this._deleteIfExpired(key, this.oldCache.get(key));
		}

		return false;
	}
	// 不会影响cache和oldCache数据,取值过期了还是会删掉的。
	peek(key) {
		if (this.cache.has(key)) {
			return this._peek(key, this.cache);
		}

		if (this.oldCache.has(key)) {
			return this._peek(key, this.oldCache);
		}
	}
	// cache删完old去删。
	delete(key) {
		const deleted = this.cache.delete(key);
		if (deleted) {
			this._size--;
		}

		return this.oldCache.delete(key) || deleted;
	}
	// 重置
	clear() {
		this.cache.clear();
		this.oldCache.clear();
		this._size = 0;
	}
	// 调整cache、oldCache中元素数量
	resize(newSize) {
		if (!(newSize && newSize > 0)) {
			throw new TypeError('`maxSize` must be a number greater than 0');
		}
		// 集合oldcache和cache,old里面找cache里没有的且没过期的,cache里找没过期的
		const items = [...this._entriesAscending()];
        // 给的newSize大于数据总量,oldcache重置,数据给cache
		const removeCount = items.length - newSize;
		if (removeCount < 0) {
			this.cache = new Map(items);
			this.oldCache = new Map();
			this._size = items.length;
		} else {
            // 容量不够的话,把old里多的进行删除回调(item前几个),剩下的全塞old里,cache重置。
			if (removeCount > 0) {
				this._emitEvictions(items.slice(0, removeCount));
			}

			this.oldCache = new Map(items.slice(removeCount));
			this.cache = new Map();
			this._size = 0;
		}

		this.maxSize = newSize;
	}
	// yield 函数,还久没碰到了
	* keys() {
		for (const [key] of this) {
			yield key;
		}
	}

	* values() {
		for (const [, value] of this) {
			yield value;
		}
	}

	* [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];
				}
			}
		}
	}

	* entriesDescending() {
		let items = [...this.cache];
		for (let i = items.length - 1; i >= 0; --i) {
			const item = items[i];
			const [key, value] = item;
			const deleted = this._deleteIfExpired(key, value);
			if (deleted === false) {
				yield [key, value.value];
			}
		}

		items = [...this.oldCache];
		for (let i = items.length - 1; i >= 0; --i) {
			const item = items[i];
			const [key, value] = item;
			if (!this.cache.has(key)) {
				const deleted = this._deleteIfExpired(key, value);
				if (deleted === false) {
					yield [key, value.value];
				}
			}
		}
	}

	* entriesAscending() {
		for (const [key, value] of this._entriesAscending()) {
			yield [key, value.value];
		}
	}
	// 搞两个cache自然就会很难算这个size,
    // 如果_size空了,返回old的
    // 如果_size有,判断old里cache没的有多少个,加上_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);
	}

	entries() {
		return this.entriesAscending();
	}

	forEach(callbackFunction, thisArgument = this) {
		for (const [key, value] of this.entriesAscending()) {
			callbackFunction.call(thisArgument, value, key, this);
		}
	}

	get [Symbol.toStringTag]() {
		return JSON.stringify([...this.entriesAscending()]);
	}
}

总结

我真的觉得这种两个map对象存值的很别扭。

set 时,如果new存满了,把数据全给old,new置空

get 时,需要 new 看一下,old也看一下

delete 同上。

size 更是 需要两边加。

虽然lur-cache代码可能更复杂点,但更好懂。

挑了不少的代码主题,因为喜欢写注释详解,找了些注释比较清晰的主题

an-old-hope 
github-gist
gml
googlecode  代码太浅
hopscotch		
lioshi
vs
xcode