vue - 用 TS 实现一个 LRU 算法
关于什么是 LRU 算法可以去看 算法就像搭乐高:带你手撸 LRU 算法 (opens new window)。
我们将使用 TypeScript 实现一个属于前端的 LRU 算法,以及在 Vue 源码(Vue2)中的使用。
# 接口定义
interface LRU<Key, Value> {
get(key: Key): Value | undefined
put(key: Key, value: Value): void
}
2
3
4
# 辅助函数
下面我们就来写一个具体的实现类,然后我们先把框架搭好:
class LRUCache<Key, Value> implements LRU<Key, Value> {
private static DEFAULT_CAPACITY = 8
private cache: Key[]
private map: Map<Key, Value>
private capacity: number
constructor(capacity: number = LRUCache.DEFAULT_CAPACITY) {
this.capacity = capacity > 0 ? capacity : LRUCache.DEFAULT_CAPACITY
this.cache = []
this.map = new Map()
}
public getCapacity() {
return this.capacity
}
public get(key: Key): Value {
throw new Error('Method not implemented.')
}
public put(key: Key, value: Value): void {
throw new Error('Method not implemented.')
}
private makeRecently(key: Key) {}
private removeLeastRecently() {}
private addRecently(key: Key, value: Value) {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们将所有的 key
存放在 cache
中,然后通过一个 map
来快速获取 value
值。
LRU 还是控制容量,我们使用 capacity
进行管理。然后通过 this.cache.length >= this.capacity
来判断当前容器是否达到上限。
这里还提供了几个私有的辅助函数,方便等下的调用。
# 实现
# 核心逻辑的实现
class LRUCache<Key, Value> implements LRU<Key, Value> {
public get(key: Key): Value | undefined {
if (!this.map.has(key)) {
return
}
this.makeRecently(key)
return this.map.get(key)
}
public put(key: Key, value: Value): void {
if (this.map.has(key)) {
this.makeRecently(key)
// 更新
this.map.set(key, value)
return
}
// 容量达到上下,删除 least recent used.
if (this.cache.length >= this.capacity) {
this.removeLeastRecently()
}
this.addRecently(key, value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对于 get
的实现:我们先判断缓存 (map) 中是否存在数据,如果不存在我们就直接返回 undefined
。否则,我们在获取之前,我们还需要进行一个更新操作(makeRecently
)。
对于 put
的实现:我们先判断缓存 (map) 是否存在数据,如果存在数据,我们需要先执行更新 makeRecently
操作,再进行返回。否则,再插入之前,我们需要看容器容量是否达到上限,
如果达到上限,我们就先删除 least recent used,然后再执行插入操作。
接下来我们就来实现几个辅助函数。
# 辅助函数的实现
class LRUCache<Key, Value> implements LRU<Key, Value> {
private makeRecently(key: Key) {
// 由于再 makeRecently 之前,我们就通过 map.has(key) 判断出 存在性。
// 所以这里可以不用判断 index === -1
const index = this.cache.indexOf(key)
this.cache.splice(index, 1)
this.cache.push(key)
}
private removeLeastRecently() {
// 由于只有当 this.cache.length >= this.capacity 才会调用该方法,、
// 所以所得到的 key 值是存在的
const key = this.cache.shift()
this.map.delete(key)
}
private addRecently(key: Key, value: Value) {
this.cache.push(key)
this.map.set(key, value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 最终代码
export interface LRU<Key, Value> {
get(key: Key): Value | undefined
put(key: Key, value: Value): void
}
export class LRUCache<Key, Value> implements LRU<Key, Value> {
private static DEFAULT_CAPACITY = 8
private cache: Key[]
private map: Map<Key, Value>
private capacity: number
constructor(capacity: number = LRUCache.DEFAULT_CAPACITY) {
this.capacity = capacity > 0 ? capacity : LRUCache.DEFAULT_CAPACITY
this.cache = []
this.map = new Map()
}
public getCapacity() {
return this.capacity
}
public get(key: Key): Value | undefined {
if (!this.map.has(key)) {
return
}
this.makeRecently(key)
return this.map.get(key)
}
public put(key: Key, value: Value): void {
if (this.map.has(key)) {
this.makeRecently(key)
// 更新
this.map.set(key, value)
return
}
// 容量达到上下,删除 least recent used.
if (this.cache.length >= this.capacity) {
this.removeLeastRecently()
}
this.addRecently(key, value)
}
private makeRecently(key: Key) {
// 由于再 makeRecently 之前,我们就通过 map.has(key) 判断出 存在性。
// 所以这里可以不用判断 index === -1
const index = this.cache.indexOf(key)
this.cache.splice(index, 1)
this.cache.push(key)
}
private removeLeastRecently() {
// 由于只有当 this.cache.length >= this.capacity 才会调用该方法,、
// 所以所得到的 key 值是存在的
const key = this.cache.shift()
this.map.delete(key)
}
private addRecently(key: Key, value: Value) {
this.cache.push(key)
this.map.set(key, value)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 测试
我们采用 jest
进行简单的测试
import { LRUCache } from './index'
describe('LRUCache', () => {
const DEFAULT_CAPACITY = 8
it('should perform the correct init', () => {
const lru = new LRUCache<number, string>()
expect(lru.getCapacity()).toBe(DEFAULT_CAPACITY)
expect(lru.get(1)).toBe(undefined)
expect(new LRUCache(-1).getCapacity()).toBe(DEFAULT_CAPACITY)
})
it('should get value correctly', () => {
const lru = new LRUCache<number, string>(2)
lru.put(1, '1')
expect(lru.get(1)).toBe('1')
expect(lru.get(2)).toBe(undefined)
lru.put(2, '2')
lru.put(3, '3')
expect(lru.get(1)).toBe(undefined)
expect(lru.get(2)).toBe('2')
expect(lru.get(3)).toBe('3')
})
it('should put data correctly', () => {
const lru = new LRUCache<number, string>(2)
lru.put(1, '1')
expect(lru.get(1)).toBe('1')
lru.put(2, '2')
lru.put(3, '3')
expect(lru.get(1)).toBe(undefined)
expect(lru.get(2)).toBe('2')
expect(lru.get(3)).toBe('3')
lru.put(4, '4')
expect(lru.get(2)).toBe(undefined)
expect(lru.get(3)).toBe('3')
lru.put(4, '44')
expect(lru.get(4)).toBe('44')
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
可以看到我们的测试是通过的(这里用 tsc 编译再测试)。
# Vue2 中的使用
Vue 在内置组件 keep-alive
就采用了 LRU 算法对缓存进行管理,对比一下 render
方法可以看到这个算法的影子:
// vue/src/core/components/keep-alive.js
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 总结
LRU 是一个高效的获取和管理数据的算法。其实我们上面的实现还可以进一步的优化,
可以将 cache
数组改用一个双向链表实现。但是对于 capacity
要求不高的
条件下,其实算法的效率也是非常快的。