前言
阅读源码的分支是 7.17.0
源码传送门:github.com/isaacs/node…
LRU 算法介绍
LRU(Least Recently Used)即最近最少使用,是一种页面置换算法。根据历史访问记录来淘汰数据,数据最近被访问,那么将来被访问的几率就更高;可用于缓存页面、缓存接口等。
实现
使用双向链表实现插入删除和 Map 实现查找
class Node {
key = null
value = null
prev = null
next = null
constructor (key, value) {
this.key = key
this.value = value
this.prev = null
this.next = null
}
}
class LRUCache {
head = null
tail = null
map = null
capacity = 0
size = 0
constructor (capacity) {
this.head = new Node(0, 0)
this.tail = new Node(0, 0)
this.head.next = this.tail
this.tail.prev = this.head
this.map = new Map()
this.capacity = capacity
}
/* 链表尾部增加节点 */
_addLast = (node) => {
node.prev = this.tail.prev
node.next = this.tail
this.tail.prev.next = node
this.tail.prev = node
++this.size
}
/* 删除节点 */
_remove = (node) => {
node.prev.next = node.next
node.prev.prev = node.prev
--this.size
}
/* 删除第一个节点 */
_removeFirst = () => {
if (this.head.next === this.tail) return null
const first = this.head.next
this._remove(first)
return first
}
/* 将某个 key 提升为最近使用的 */
_makeRecently = (key) => {
const node = this.map.get(key)
this._remove(node)
this._addLast(node)
}
/* 添加最近使用的元素 */
_addRecently = (key, value) => {
const node = new Node(key, value)
this._addLast(node)
this.map.set(key, node)
}
/* 删除某一个 key */
_deleteKey = (key) => {
const node = this.map.get(key)
this._remove(node)
this.map.delete(key)
}
/* 删除最久未使用的元素 */
_removeLeastRecently = () => {
const node = this._removeFirst()
this.map.delete(node.key)
}
/* 链表长度 */
getSize = () => {
return this.size
}
get = (key) => {
if (!this.map.has(key)) return -1
this._makeRecently(key)
return this.map.get(key).value
}
put = (key, value) => {
if (this.map.has(key)) {
this._deleteKey(key)
this._addRecently(key, value)
return
}
if (this.capacity === this.getSize()) {
this._removeLeastRecently()
}
this._addRecently(key, value)
}
}
lru-cache 介绍
一个可以在跨平台使用且关于LRU实现的缓存对象,不侧重于 TTL 缓存;在 v7 版本后,是 JavaScript 中性能高的 LRU 实现之一。
基础用法
import LRUCache from 'lru-cache'
const cache = new LRUCache({ max: 500 })
cache.set('key', 'value')
cache.get('key')
源码
使用 class 语法糖,在 constructor 函数中,对一系列内部变量进行初始化及参数的校验。
// UintArray = Uint8Array \ Unit16Array \ Unit32Array \ ZeroArray
// constructor 函数
this.keyList = new Array(max).fill(null)
this.valList = new Array(max).fill(null)
this.next = new UintArray(max)
this.prev = new UintArray(max)
this.initialFill = 1
- 缓存键、值分别使用数组构造函数初始化存储。个人理解是相对于使用字面量更节省内存,直接使用数组下标来改变数组的元素,而不是使用
push - 每个节点指向前节点和后节点的 “指针” 分别使用 n 位无符号整数数组存储(一次性分配连续内存块;避免在存储量级大数据时,增加下标内存动态分配造成的消耗)
- 不使用 哈希表 和 双端队列 实现,并且键、值、值的上节点、值的下节点分别用数组存储
set(key, value, [{ size, sizeCalculation, ttl, noDisposeOnSet, start }])
加入一个缓存数据。分为两种情况:缓存对象里面已存在相同 key 、增加新缓存数据。
增加新缓存数据
// set add
set(
k,
v,
{
ttl = this.ttl,
start,
noDisposeOnSet = this.noDisposeOnSet,
size = 0,
sizeCalculation = this.sizeCalculation,
noUpdateTTL = this.noUpdateTTL,
} = {}
) {
// 数据项超过给定的总体积时删除
size = this.requireSize(k, v, size, sizeCalculation)
if (this.maxEntrySize && size > this.maxEntrySize) {
this.delete(k)
return this
}
// 获取可插入的索引
index = this.newIndex()
this.keyList[index] = k
this.valList[index] = v
this.keyMap.set(k, index)
this.next[this.tail] = index
this.prev[index] = this.tail
this.tail = index
this.size++
this.addItemSize(index, size)
noUpdateTTL = false
// ...
newIndex() {
if (this.size === 0) {
return this.tail
}
// 设置的最大个数已满
if (this.size === this.max && this.max !== 0) {
// 收回最近最少使用的内部ID
return this.evict(false)
}
// 重复利用被删除的内部ID
if (this.free.length !== 0) {
return this.free.pop()
}
return this.initialFill++
}
evict(free) {
const head = this.head
const k = this.keyList[head]
const v = this.valList[head]
if (this.isBackgroundFetch(v)) {
v.__abortController.abort(new Error('evicted'))
} else {
this.dispose(v, k, 'evict')
if (this.disposeAfter) {
this.disposed.push([v, k, 'evict'])
}
}
this.removeItemSize(head)
// if we aren't about to use the index, then null these out
if (free) {
this.keyList[head] = null
this.valList[head] = null
this.free.push(head)
}
this.head = this.next[head]
this.keyMap.delete(k)
this.size--
return head
}
newIndex方法是获取新增数据的数组填充下标。当已有数据个数等于初始化最大数据个数时,进入到evictevict方法获取最近最少使用的数据数组下标。首先使用isBackgroundFetch判断是否该数据正在从后台获取,如果是中止并抛出事件(前提是已使用cache.fetch)。否则调用dispose,这是删除数据项时的回调函数,可由用户覆盖。而disposeAfter则是在完全删除数据项后调用free.pop的一种情况是delete时使用栈回收相关数组的索引下标,后续set等操作就可以从中获取
isBackgroundFetch(p) {
return (
p &&
typeof p === 'object' &&
typeof p.then === 'function' &&
Object.prototype.hasOwnProperty.call(
p,
'__staleWhileFetching'
) &&
Object.prototype.hasOwnProperty.call(p, '__returned') &&
(p.__returned === p || p.__returned === null)
)
}
Object.prototype.hasOwnProperty:isBackgroundFetch 参数可能是使用 Object.create(null) 创建或者 hasOwnProperty 被覆盖。
// set
this.next[this.tail] = index
this.prev[index] = this.tail
this.tail = index
next存储指向下个节点的指针;ID=0(理解成数组的第一个元素)对应的是valList第一个元素的下个节点ID(理解成数组下标索引),其它的以此类推。最后一个节点的下个节点ID指向 0prev索引0 存放的是valList第一个元素的上节点ID=0,恒为 0;索引2 是valList第二个元素的上节点ID,指向第一个元素的下标,为0tail指向最近最多使用的数据项索引
缓存对象里面已存在相同 key
// set update
this.moveToTail(index)
const oldVal = this.valList[index]
if (v !== oldVal) {
if (this.isBackgroundFetch(oldVal)) {
oldVal.__abortController.abort(new Error('replaced'))
} else {
// 重复 set 时调用 dispose
if (!noDisposeOnSet) {
this.dispose(oldVal, k, 'set')
if (this.disposeAfter) {
this.disposed.push([oldVal, k, 'set'])
}
}
}
this.removeItemSize(index)
this.valList[index] = v
this.addItemSize(index, size)
// moveToTail
if (index !== this.tail) {
// 更新值的索引位于头部
if (index === this.head) {
this.head = this.next[index]
} else {
// 更新当前节点上节点的 next 指针、下节点的 prev 指针
this.connect(this.prev[index], this.next[index])
}
// 更新当前节点的 next 指针、prev 指针
this.connect(this.tail, index)
this.tail = index
}
connect(p, n) {
this.prev[n] = p
this.next[p] = n
}
get(key, { updateAgeOnGet, allowStale } = {}) => value
获取缓存数据项
const index = this.keyMap.get(k)
if (index !== undefined) {
const value = this.valList[index]
const fetching = this.isBackgroundFetch(value)
// 当前分支 isStale 方法总返回 false
if (this.isStale(index)) {
if (!fetching) {
if (!noDeleteOnStaleGet) {
this.delete(k)
}
return allowStale ? value : undefined
} else {
return allowStale ? value.__staleWhileFetching : undefined
}
} else {
if (fetching) {
return undefined
}
this.moveToTail(index)
if (updateAgeOnGet) {
this.updateItemAge(index)
}
return value
}
}
fetching = true正在获取中的且可认为还没过期,返回undefined
fetch方法阅读待续...
参考资料
有错误或者可以改进的地方请不吝赐教