- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第xx期,链接: juejin.cn/post/707975… 。
- 学习连接: juejin.cn/post/708266…
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 属性的属性特性: | |
|---|---|
| writable | false |
| enumerable | false |
| configurable | false |
描述
许多内置的 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, 如下图所示: