keep-alive实现原理

LRU缓存-keep-alive实现原理

keep-alive是Vue.js的一个内置组件。它能够将不活动的组件实例保存在u内存中,而不是直接销毁,它是一个抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。简单地说,keep-alive用于保存组件的渲染状态,避免组件反复创建和渲染,有效提高系统性能。

keep-alivemax属性,用于限制可以缓存多少组件实例,一旦这个数字达到了上限,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉

LRU缓存淘汰算法

LRU缓存淘汰算法:根据数据的历史记录来淘汰数据,重点在于保护最近被访问/使用过的数据,淘汰阶段最久被访问的数据

主体思想:如果数据最近被访问过,那么将来被访问的几率也更高

img

1、新数据插入到链表尾部

2、每当命中缓存(即缓存数据被访问),则将数据移到链表尾部

3、当链表满的时候,将链表头部数据丢弃

实现LRU的数据结构

hashMap+双向链表 考虑可能需要频繁删除一个元素,并将这个元素的前一个结点指向下一个节点,所以使用双链接最合适

class LRUCache {
        capacity; //容量
        cache; //缓存
        constructor(capacity) {
          this.capacity = capacity;
          this.cache = new Map();
        }
        get(key) {
          if (this.cache.has(key)) {
            let temp = this.cache.get(key);
            this.cache.delete(key);
            this.cache.set(key, temp);
            return map;
          }
          return -1;
        }
        put(key, value) {
          if (this.cache.has(key)) {
            this.cache.delete(key);
          } else if (this.cache.size >= this.capacity) {
            this.cache.delete(this.cache.keys().next().value);
          }
          this.cache.set(key, value);
        }
      }

Vue中的Keep-Alive

原理:

1.使用LRU缓存机制进行缓存,max限制缓存表的最大容量

2.根据设定的include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配直接返回组件实例

3.根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接去除缓存值并更新该key再this.keys中的位置(更新key的位置是实现LRU置换策略的关键)

4.获取节点名称,或者根据节点cid等信息喷出当前组件名称

5.获取keep-alive包裹着第一个子组件对象及其组件名

const KeepAliveImpl = {
        name: "KeepAlive",
        props: {
          include: [String, RegExp, Array],
          exclude: [String, RegExp, Array],
          max: [String, Number],
        },
        setup(props, { slots }) {
          const cache = new Map();
          const keys = new Set();
          let current = null;
          //当props上的include或者exclude变化时移除缓存
          watch(
            () => [propos.include, props.exclude],
            ([include, exclude]) => {
              include && pruneCache((name) => matches(include, name));
              exclude && pruneCache((name) => !matches(exclude, name));
            },
            { flush: "post", deep: true }
          );
          let pendingCacheKey = null;
          const cacheSubtree = () => {
            if (pendingCacheKey != null) {
              cache.set(pendingCacheKey, getInnerChild(instance.subTree));
            }
          };
          onMounted(cacheSubtree);
          onUpdated(cacheSubtree);
          onBeforeUnmount(() => {
            // 卸载缓存表里的所有组件和其中的子树...
          });
          return () => {
            // 省略部分代码,以下是缓存逻辑
            pendingCacheKey = null;
            const children = slots.default();
            let vnode = children[0];
            const comp = vnode.type;
            const name = getName(comp);
            const { include, exclude, max } = props;
            // key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
            const key = vnode.key == null ? comp : vnode.key;
            // 通过 key 值获取缓存节点
            const cachedVNode = cache.get(key);
            if (cachedVNode) {
              // 缓存存在,则使用缓存装载数据
              vnode.el = cachedVNode.el;
              vnode.component = cachedVNode.component;
              if (vnode.transition) {
                // 递归更新子树上的 transition hooks
                setTransitionHooks(vnode, vnode.transition);
              }
              // 阻止 vNode 节点作为新节点被挂载
              vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;
              // 刷新key的优先级
              keys.delete(key);
              keys.add(key);
            } else {
              keys.add(key);
              // 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
              if (max && keys.size > parseInt(max, 10)) {
                pruneCacheEntry(keys.values().next().value);
              }
            }
            // 避免 vNode 被卸载
            vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
            current = vnode;
            return vnode;
          };
        },
      };
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent);
    if (name && (!filter || !filter(name))) {
      // !filter(name) 即 name 在 includes 或不在 excludes 中
      pruneCacheEntry(key);
    }
  });
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode;
  if (!current || cached.type !== current.type) {
    /* 当前没有处在 activated 状态的组件
     * 或者当前处在 activated 组件不是要删除的 key 时
     * 卸载这个组件
    */
    unmount(cached); // unmount方法里同样包含了 resetShapeFlag
  } else if (current) {
    // 当前组件在未来应该不再被 keepAlive 缓存
    // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
    resetShapeFlag(current);
    // resetShapeFlag 
  }
  cache.delete(key);
  keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
  let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
   // ... 清除组件的 shapeFlag
}

总结

使用 KeepAlive 后,被 KeepAlive 包裹的组件在经过第一次渲染后,它的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。

缓存过程:

1.声明有序集合keys作为缓存容器,存入组件的唯一key值
2.在缓存容器keys中,越靠前的key值越意味着被访问的越少也越优先被淘汰
3.渲染函数执行时,若命中缓存时,则从keys中删除当前命中的key,并往keys末尾追加key值,刷新该key的优先级
4.未命中缓存时,则 keys 追加缓存数据 key 值,若此时缓存数据长度大于 max 最大值,则删除最旧的数据
5.当触发beforeMount/update生命周期,缓存当前activated组建的子树的数据

<think>我们正在讨论Vue3的keep-alive组件实现原理。根据引用[1],我们知道在Vue3中,keep-alive通过激活(activate)和失效(deactivate)两个函数与渲染器实例进行通信,并且这些函数是通过KeepAlive实例的ctx属性传递的,以避免在渲染器中直接导入KeepAlive从而影响tree-shaking。 接下来,我们需要深入Vue3最新版本(假设为3.4.x)的源码来详细解释keep-alive实现。注意,我们将基于Vue3的源码结构进行讲解,重点在keep-alive组件的实现机制。 1. **KeepAlive组件的位置和结构**: 在Vue3源码中,KeepAlive组件的实现位于`packages/runtime-core/src/components/KeepAlive.ts`文件中。这个文件包含了`KeepAlive`组件的定义。 2. **KeepAlive的渲染和缓存机制**: - KeepAlive组件是一个带`__isKeepAlive`标志的组件。 - 它通过一个Map(或类似缓存对象)来存储被缓存的组件实例。在Vue3中,缓存对象是一个`Cache`实例,它内部使用`Map`来存储vnode的键和对应的vnode。 - 当组件被包裹在`<keep-alive>`中时,它的vnode会被标记,并且在组件卸载(deactivated)时不会被销毁,而是被放入缓存中。当需要重新激活(activated)时,会从缓存中取出并挂载。 3. **激活和失效的生命周期**: -缓存的组件被重新插入到DOM中时,会触发`activate`函数,该函数会调用缓存的组件实例的激活钩子(如果存在)。 -缓存的组件被移除时(但未被销毁,只是失活),会触发`deactivate`函数,该函数会调用缓存的组件实例的失活钩子。 4. **与渲染器的通信**: - 渲染器在挂载和更新组件时,会检查vnode的类型。如果遇到KeepAlive组件,渲染器会调用KeepAlive组件暴露的`activate`和`deactivate`方法。这些方法通过KeepAlive实例的`ctx`属性传递给渲染器。 5. **避免tree-shaking问题**: - 引用[1]中提到,将激活和失效的实现通过ctx属性传递给渲染器,而不是直接在渲染器中导入KeepAlive,这样可以避免在未使用KeepAlive时,由于tree-shaking而无法移除KeepAlive的代码。 6. **缓存的管理**: - KeepAlive组件通过一个缓存对象(`cache: Map<Key, VNode>`)来存储vnode。每个被缓存的vnode都有一个key(通常基于组件的类型和props生成)。 -缓存数量超过`max`属性指定的值时,会使用LRU(最近最少使用)策略来清除缓存。 7. **最新的源码实现细节**: - 在最新版本中,KeepAlive组件使用`setup`函数式组件编写。 - 它定义了一个缓存对象,以及两个内部函数`activate`和`deactivate`,这两个函数会被赋值给实例的`ctx`属性。 - 在渲染过程中,KeepAlive组件的`render`函数会返回它的第一个子节点(即被缓存的组件)。在子节点被挂载和卸载时,会触发相应的激活和失活逻辑。 下面,我们结合源码(简化)来具体说明: ```typescript // 简化版 KeepAlive 实现 export const KeepAlive = defineComponent({ __isKeepAlive: true, props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], max: [String, Number] }, setup(props, { slots }) { const cache: Cache = new Map() const keys: Key[] = [] const instance = getCurrentInstance()! // 当前组件实例 // 设置渲染器相关的激活/失活函数 instance.ctx.activate = (vnode, container, anchor) => { // 激活逻辑:将缓存的vnode插入到容器中 // 并调用vnode的组件实例上的activated钩子 } instance.ctx.deactivate = (vnode) => { // 失活逻辑:将vnode从容器中移除,但保留在缓存中 // 并调用vnode的组件实例上的deactivated钩子 } return () => { // 渲染函数:返回第一个子节点(需要被缓存的组件) const children = slots.default?.() if (!children || !children.length) return const rawVNode = children[0] // ... 处理缓存逻辑,包括匹配include/exclude,管理缓存大小等 return rawVNode } } }) ``` 在渲染器(在`packages/runtime-core/src/renderer.ts`中)处理KeepAlive组件时,会通过`instance.ctx`获取到`activate`和`deactivate`函数,并在适当的时候调用它们。 总结:Vue3的keep-alive通过内部缓存机制和与渲染器的协作,实现了组件的状态保留。它利用激活和失活函数来管理组件的挂载和卸载,同时通过ctx属性避免直接依赖,优化了tree-shaking。 §§相关问题§§ 1. Vue3中keep-alive缓存策略是什么?如何管理缓存大小? 2. keep-alive如何与Vue的渲染器协作? 3. 在Vue3中,keep-alive是如何避免影响tree-shaking的? 4. keep-alive在激活和失活时分别触发了哪些生命周期钩子? </think> ### Vue3 keep-alive 实现原理解析(基于最新源码) 在 Vue3 中,`<keep-alive>` 是一个内置组件,用于缓存不活动的组件实例,避免重复渲染。其核心原理通过以下机制实现--- #### 一、缓存机制实现 1. **缓存数据结构** 源码使用 `Map` 对象存储缓存的虚拟 DOM 节点(vnode): ```typescript const cache: Map<string, VNode> = new Map() const keys: Set<string> = new Set() // 管理 LRU 策略的键集合 ``` 2. **LRU 缓存淘汰策略** 当缓存数量超过 `max` 属性限制时(默认无限制),移除最久未使用的缓存: ```typescript if (max && keys.size > parseInt(max)) { pruneCacheEntry(keys.values().next().value) } ``` --- #### 二、激活/失活生命周期 1. **组件激活流程** -缓存中取出 vnode,调用渲染器的 `activate` 函数挂载组件 - 触发组件的 `activated` 生命周期钩子 ```typescript instance.ctx.activate = (vnode, container, anchor) => { // 实际挂载操作 mountComponent(vnode, container, anchor) // 触发 activated 钩子 [^1] } ``` 2. **组件失活流程** - 通过渲染器的 `deactivate` 函数卸载组件(保留 DOM 结构) - 触发组件的 `deactivated` 生命周期钩子 ```typescript instance.ctx.deactivate = (vnode) => { // 移除 DOM 但保留组件实例 unmountComponent(vnode) // 触发 deactivated 钩子 [^1] } ``` --- #### 三、渲染器通信机制 为避免直接导入 `KeepAlive` 影响 tree-shaking,Vue3 通过渲染器上下文(`ctx`)注入激活/失活函数: ```typescript // 在渲染器中注册 KeepAlive renderer.ctx.keepAlive = { activate: (vnode, container, anchor) => { /*...*/ }, deactivate: (vnode) => { /*...*/ } } ``` 这种设计确保未使用 `keep-alive` 时相关代码可被 tree-shaking 移除。 --- #### 四、缓存匹配策略 1. **`include/exclude` 过滤** 通过 `match` 函数匹配组件名称: ```typescript const isMatch = (name: string) => (include && !matches(include, name)) || (exclude && matches(exclude, name)) ``` 2. **缓存 key 生成** 根据组件类型和 props 生成唯一标识: ```typescript const key = vnode.type + (vnode.key ? '::' + vnode.key : '') ``` --- #### 五、性能优化设计 1. **避免重复渲染** 被缓存的组件切换时直接复用实例,跳过 `created/mounted` 钩子。 2. **DOM 结构保留** 失活时通过 `deactivate` 保留 DOM 节点: ```typescript function unmountComponent(vnode) { vnode.el._leaveCb?.() // 保留 DOM 引用 } ``` > 源码位置:`packages/runtime-core/src/components/KeepAlive.ts`
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值