通过LRU理解Vue3中keep-alive部分源码

423 阅读4分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

keep-alive标签包裹的组件在切换时可以保留状态避免重新渲染。

但是缓存不能是无限的,否则会挤爆内存,有三种形式去进行缓存的淘汰。

1、 FIFO 队列。这个更适合做任务,不适合做组件的。

2、LFU。统计每个任务出现的次数,来淘汰出现次数较少的。缺点是比较耗费计算资源。

3、LRU缓存。全称Least Recently Used。也就是最近最少使用。

把所有元素按最近使用情况排序,比如已经到达固定存储量,添加新元素时需要判断当前缓存中是否存在相同元素,若有相同元素则将相同元素放到最后(更新),若无相同元素则添加该元素并删除第一个元素(也就是最早添加的)。

例子: 最大长度为4的缓存

当前缓存添加元素更新后缓存
1,2,3,421,3,4,2
1,2,341,2,3,4

组件缓存用的最多的就是LRU缓存。(LRU Cache)

源码文件对应代码段

在Vue的源码文件中的packages/runtime-core/src/components/KeepAlive.ts中就有用到,采用的是set的实现方式。

setup函数

声明cache(Map)和keys(Set)

Image.png

pruneCacheEntry(超出大小时,用于淘汰缓存)

若新增时超出缓存最大长度,需要用pruneCacheEntry删除keys中最早添加的key对应到cache中的数据。

Image.png

在组件挂载或者更新时,向cache中新增数据

渲染函数(return ()=> {...})中为pendingCacheKey赋值

Image.png

执行 render 的时候,pendingCacheKey 会被赋值为 vnode.key

在组件挂载或者更新(onMouted, onUpdated)时,向cache中新增 <key, vnode>

Image.png

setup函数中return的渲染函数

获取子节点vnode和key

获取到当前keep-alive包裹的子节点

Image.png

Image.png

获取到当前节点的key值

Image.png

通过cachedNode判断

通过cachedNode(cache中是否已经有这个key对应的vnode)来判断是否当前被组件是否已被缓存过。

  1. 若以被缓存过,则更新被缓存的节点到keys的末尾(更新)
  2. 若未被缓存过
    1. 向keys中添加当前node的key
    2. 判断是否超出缓存最大值,若超出缓存最大值则调用pruneCacheEntry传入最早add进keys的key作为参数,在pruneCacheEntry中cache根据入参的key来删除keys和cache对应数据。

Image.png

Image.png

总结:

可以看出是通过cache(map)和keys(set)配合实现了缓存功能。本质上可以说实际上是对keys实现了LRU算法,只是通过keys中的key确保cache中缓存数据没问题。

在setup函数返回的渲染函数中。

  1. 获取当前keep-alive的子节点以及key。
  2. 当缓存中存在与当前组件相同的key时,keys将该数据更新到末尾(将相同key的数据先删除后添加,达到更新的效果)。
  3. 若无相同key的节点已缓存,则直接向keys中add当前的key。同时需要判断是否超过缓存最大长度,若超过长度则通过pruneCacheEntry函数淘汰(删除)最早add进keys的key和该key在cache中对应的数据。

当组件挂载或更新(onMounted和onUpdated)时触发cacheSubtree,给cache添加当前key和vnode。

Map实现

  1. 通过has判断是否存在相同的数据(也就是判断当前节点和缓存过的节点是否相同)。
  2. 通过map.keys().next().value可以获取到第一位元素(这是由于map.keys()返回的是一个迭代器,可以通过next拿到第一个值)可以获取到map中最先放入的key。也就意味着可以通过delete这个key来淘汰最早set进去的key。
let cache = new Map()
cache.set('a',1)
cache.set('b',2)
cache.set('c',3)
console.log(cache.keys().next().value) // a 也就是最先放入的key
  1. set新的key是有序的,可以删除掉相同的key然后再次set达到将相同值更新到map末尾的效果。

满足实现LRU算法的条件。以下是leetcode算法题通过Map的实现。

LRU算法题

力扣

var LRUCache = function(capacity) {
    this.cache = new Map()
    // max赋值为缓存最大大小
    this.max = capacity
};

LRUCache.prototype.get = function(key) {
    if(this.cache.has(key)){
        // 用临时变量存储键值
        let tmp = this.cache.get(key)
        // 删除原来的键和值,再次添加,达到更新到末尾的效果
        this.cache.delete(key)
        this.cache.set(key,tmp) 
        return tmp 
    }
    return -1
};

LRUCache.prototype.put = function(key, value) {
    if(this.cache.has(key)){
        this.cache.delete(key)
       // 当当前cache大小超出最大时需要有缓存淘汰机制
    } else if(this.cache.size>=this.max) {
       // 删除map头部元素,也就是最开始set的key
        this.cache.delete(this.cache.keys().next().value)
    }
    this.cache.set(key,value)
    return
};