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

155 阅读8分钟

LRU是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

理论

2.1 第一个图

如上图所示,存储空间只能放3个页,LRU算法执行过程如下:

(1)当引用页面7时,没命中(没在存储空间中),存储空间有位置,放进去;

(2)当引用页面0时,没命中(没在存储空间中),存储空间有位置,放进去;

(3)当引用页面1时,没命中(没在存储空间中),存储空间有位置,放进去;

(4)当引用页面2时,没命中(没在存储空间中),存储空间没有位置(专业术语叫“缺页”),由于7是最近没有用的,把它置换出去,2放进去;

(5)当引用页面0时,命中了(在存储空间中),页面0属于最近被使用的;

(6)当引用页面3时,没命中(没在存储空间中),存储空间没有位置,页面0和2是最近被使用的,页面1是最近没被使用的,把它置换出去,3放进去。其他以此类推...

2.2 第二个图

可利用一个特殊的栈来保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它压入栈顶。因此,栈顶始终是最新被访问页面的编号,而栈底则是最近最久未使用页面的页面号。则LRU算法执行过程如下:

(1)引用页面4时,没命中,存储空间有位置,放进去;

(2)引用页面7时,没命中,存储空间有位置,放进去,7是最近被访问的,放在栈顶;

(3)引用页面0时,没命中,存储空间有位置,放进去,0是最近被访问的,放在栈顶;

(4)引用页面7时,命中了,7是最近被访问的,放在栈顶;

(5)引用页面1时,没命中,存储空间有位置,放进去,1是最近被访问的,放在栈顶;

(6)引用页面0时,命中了,0是最近被访问的,放在栈顶;

(7)引用页面1时,命中了,1是最近被访问的,放在栈顶;

(8)引用页面2时,没命中,存储空间有位置,放进去,2是最近被访问的,放在栈顶;

(9)引用页面1时,命中了,1是最近被访问的,放在栈顶;

(10)引用页面2时,命中了,1是最近被访问的,放在栈顶;

(11)引用页面1时,没命中,存储空间没位置(缺页了),把栈底的4(最近最不经常访问的)置换出去,6放进去,6是最近被访问的,放在栈顶;

使用场景

  • 在vue中,keep-alive组件的实现

问题

1.怎么知道访问的页面在不在存储空间中,应该用什么数据结构?

2.怎么标识谁是最近被访问的,应该用什么数据结构?如何设计?

解法1:

Map 这个 ES6 新增数据结构是 有序的,即不仅可以存储键值对(关键字-值), 还可以通过 delete 和 set 的 API 对数据进行更新,最后 set 的数据默认在 Map 的末尾

quick-lru

用法

import QuickLRU from 'quick-lru';

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

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

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

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

源代码

export default class QuickLRU extends Map {
	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');
		}

		// TODO: Use private class fields when ESLint supports them.
		this.maxSize = options.maxSize;
		this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
		this.onEviction = options.onEviction;
    //现在的cache
		this.cache = new Map();
    //老的cache,用于交换
		this.oldCache = new Map();
		this._size = 0;
	}

	//从cache中移除所有元素
  //从cache缓存参数中移除所有元素会调用此方法,触发用户传的onEviction方法
	_emitEvictions(cache) {
		if (typeof this.onEviction !== 'function') {
			return;
		}

		for (const [key, item] of cache) {
      //cache中的每一个元素触发一次onEviction方法
			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);
			}
    	//delete()方法删除元素
			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;
	}

  //从cache中获取元素的值
	_peek(key, cache) {
		const item = cache.get(key);

		return this._getItemValue(key, item);
	}

  //设置元素放入cache
	_set(key, value) {
    // 设置元素的时候会优先向cache中设置
		this.cache.set(key, value);
		this._size++;
		//检查_size是否大于等于maxSize
		if (this._size >= this.maxSize) {
			this._size = 0;
      //调用_emitEvictions移除oldCache中所有的元素
			this._emitEvictions(this.oldCache);
      //oldCache赋值为cache
			this.oldCache = this.cache;
     	//cache更新为一个新的Map
			this.cache = new Map();
		}
	}

  //元素设置为最近使用的
	_moveToRecent(key, item) {
    //从oldCache中删除这个元素
		this.oldCache.delete(key);
    //_set放入cache
		this._set(key, item);
	}

  //迭代元素,一个生成器函数
	* _entriesAscending() {
    //优先遍历老cache
		for (const item of this.oldCache) {
			const [key, value] = item;
      //新的cache不存在key
			if (!this.cache.has(key)) {
        //检查是否过期
				const deleted = this._deleteIfExpired(key, value);
        //没过期则返回
				if (deleted === false) {
					yield item;
				}
			}
		}

    //遍历新cache
		for (const item of this.cache) {
			const [key, value] = item;
			const deleted = this._deleteIfExpired(key, value);
			if (deleted === false) {
				yield item;
			}
		}
	}

  //获取元素
	get(key) {
    //优先从cache中获取,chache有则返回元素
		if (this.cache.has(key)) {
			const item = this.cache.get(key);

			return this._getItemValue(key, item);
		}
  	//新cache中没有,老的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(key, value, {maxAge = this.maxAge} = {}) {
    //设置到期日期
		const expiry =
			typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY ?
				Date.now() + maxAge :
				undefined;
    //cache中是否已经存在元素
		if (this.cache.has(key)) {
      //已经存在则更新
			this.cache.set(key, {
				value,
				expiry
			});
		} else {
      //_set设置元素
			this._set(key, {value, expiry});
		}
	}

  //是否含有元素
	has(key) {
    优先cache且没有过期
		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;
	}

  //获取元素,但是不标记为最近使用
	peek(key) {
		if (this.cache.has(key)) {
			return this._peek(key, this.cache);
		}

		if (this.oldCache.has(key)) {
			return this._peek(key, this.oldCache);
		}
	}

  //删除元素
	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;
	}

  //调整缓存大小
	resize(newSize) {
		if (!(newSize && newSize > 0)) {
			throw new TypeError('`maxSize` must be a number greater than 0');
		}

   	//调用this._entriesAscending()获取所有的元素,
		const items = [...this._entriesAscending()];
    //比较元素数目和newSize的大小
		const removeCount = items.length - newSize;
    //小于0说明,说明newSize大,保留items都存进cache中,oldCache清空。
		if (removeCount < 0) {
			this.cache = new Map(items);
			this.oldCache = new Map();
			this._size = items.length;
		} else {
      //如果差值大于0说明newSize小,要删除元素,则把剩余元素放入oldCache,清空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;
	}

  //获取key
	* keys() {
		for (const [key] of this) {
			yield key;
		}
	}
  
 //获取value
	* values() {
		for (const [, value] of this) {
			yield value;
		}
	}

  //遍历器,优先cache
	* [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];
				}
			}
		}
	}

		//生成器,获取olccache和cache中没有过期的元素
	* 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];
		}
	}

	//得到缓存长度
	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()]);
	}
}

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

Symbol.toStringTag 属性的属性特性:
writablefalse
enumerablefalse
configurablefalse

描述

许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签,比如:

Object.prototype.toString.call('foo');     // "[object String]"
Object.prototype.toString.call([1, 2]);    // "[object Array]"
Object.prototype.toString.call(3);         // "[object Number]"
Object.prototype.toString.call(true);      // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null);      // "[object Null]"

test('.get() / .set()', t => { const lru = new QuickLRU({maxSize: 100}); lru.set('foo', 1); lru.set('bar', 2); t.is(lru.get('foo'), 1); t.is(lru.size, 2); }); 复制代码

调试过程

测试用例是test.js (quick-lru/test.js),以两个测试用例的执行过程来加深对quick-lru的理解,关于如何调试可以参考川哥的文章(juejin.cn/post/703058…)。

第一个测试用例

test('.get() / .set()', t => {
	const lru = new QuickLRU({maxSize: 100});
	lru.set('foo', 1);
	lru.set('bar', 2);
	t.is(lru.get('foo'), 1);
	t.is(lru.size, 2);
});

测试用例执行到lru.set('foo', 1)时会调用set方法,此时key为foo, value为1,由于没有指定maxAge所以maxAge值为Infinity,如下图:

执行set方法时又会调用_set方法,此时cache和oldCache都为空的map ,如下图:

执行_set时, cache执行set,参数key为foo,值为由value和expiry组成的对象,_size加1.

执行lru.set('bar', 2)时,最终也会调用_set方法,设置cache, _size加1值为2,由于maxSize为100,所以接下来的逻辑不再执行。

第二个测试用例

test('.get() - limit', t => {
	const lru = new QuickLRU({maxSize: 2});
	lru.set('1', 1);
	lru.set('2', 2);
	t.is(lru.get('1'), 1);
	t.is(lru.get('3'), undefined);
	lru.set('3', 3);
	lru.get('1');
	lru.set('4', 4);
	lru.get('1');
	lru.set('5', 5);
	t.true(lru.has('1'));
});

当执行到lru.set('2', 2)时,cache中元素个数已经达到maxSize, 所以if语句的条件成立,会执行分支内的语句,如下图所示:

执行if分支语句内的逻辑时,cache中的元素会赋值给oldCache,同时将cache设置为空的Map,如下图所示:

当执行t.is(lru.get('1'), 1)时,调用get方法,首先在cache中查找,由于cache已经为空,所以继续在oldCache中查找,如下图所示:

从oldCache中找到,就说明这个元素被访问了,那么要把它移入到cache中,代表最近访问它了,这是通过_moveToRecent函数实现的,如下图所示:

执行完_moveToRecent函数之后,1对应的元素被移入了cache,此时cache和oldCache中各一个元素:

执行 t.is(lru.get('3'), undefined);会调用get方法,获取3对应的元素,在cache和oldCache中都没有查找到,返回undefined,如下图所示:

执行lru.set('3', 3)时,也会最终调用_set方法,首先将值设置到cache中,此时cache中有两个元素,oldCache中有一个元素, 如下图所示:

_set接着往下执行,此时_size大于maxSize所以oldCache赋值为cache, cache初始化为新的map, 如下图所示: