【源码共读】| quick-lru

339 阅读3分钟

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

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

【若川视野 x 源码共读】第29期 | quick-lru 点击了解本期详情一起参与

今天阅读的库是:quick-lru

image-20221028114504173

什么是LRU算法

操作系统中常用的分页置换算法

  • 为了提高页面命中率,发生缺页中断时,选择未使用时间最长的页面置换出去。
  • 相当于一个缓存的机制

image-20221028114125582

使用场景

源码分析

package.json查看入口文件

image-20221028134059645

测试用例

我们来看下源码怎么实现的,首先打开测试用例

// 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+双向链表的形式?

这个与**HashMap+ 双向链表** 有什么区别

  • 得益于JSMap设计,在ES6之后,Map的数据结构是有序的,即Map可以模拟出双向链表的操作

这个算法使用了两个Map来作为缓存,算法复杂度为O(1),空间换时间,效率更高。


相关算法题