Skip to content

KeepAlive:组件缓存与状态保持的策略

引言

在 Vue 中,使用动态组件(<component :is="view">)或 v-if 切换组件时,Vue 的默认行为是**卸载(unmount)旧组件,并挂载(mount)**新组件。这个过程会销毁旧组件的实例,导致其内部状态(如表单数据、滚动位置)全部丢失。

<KeepAlive> 是一个内置组件,它的主要作用是解决这个问题。它会“捕获”本应被卸载的组件 VNode,将其“缓存”在内存中,并在下次需要时“激活”它,而不是重新创建。


核心:setup 函数与缓存系统

<KeepAlive> 的所有逻辑都定义在它的 setup 函数中 (packages/runtime-core/src/components/KeepAlive.ts)。它通过 setup 创建并管理一个缓存系统,并返回一个自定义的 render 函数来控制渲染流程。

typescript
// core/packages/runtime-core/src/components/KeepAlive.ts
setup(props: KeepAliveProps, { slots }: SetupContext) {
  const instance = getCurrentInstance()!
  
  // 1. 【缓存核心】
  // cache: Map<CacheKey, VNode>
  //    - 键 (key): 组件的 key 或 type
  //    - 值 (value): 缓存的 VNode 实例
  const cache: Cache = new Map()

  // keys: Set<CacheKey>
  //    - 用于 LRU 算法,存储键的“访问顺序”
  const keys: Keys = new Set()

  // 2. 【DOM 存储】
  // 创建一个隐藏的 <div>,用作“存储容器”
  // 所有“去激活”的组件 DOM 都会被移动到这里
  const storageContainer = createElement('div')

  // 3. 【渲染器接口】
  // 获取底层的渲染器方法
  const {
    renderer: {
      p: patch, // patch
      m: move,  // move (DOM 移动)
      um: _unmount, // unmount
      o: { createElement },
    },
  } = (instance.ctx as KeepAliveContext).renderer
  
  // 4. 【定义“激活”与“去激活”动作】
  
  // “去激活”:把组件移入“存储容器”
  function deactivate(vnode: VNode) {
    // 移动 DOM 节点到 storageContainer
    move(vnode, storageContainer, null, MoveType.LEAVE)
    // 异步执行 onDeactivated 钩子
    queuePostRenderEffect(() => {
      instance.da && invokeArrayFns(instance.da) // 'da' -> onDeactivated
      instance.isDeactivated = true
    }, parentSuspense)
  }

  // “激活”:把组件移出“存储容器”,放回页面
  function activate(vnode, container, anchor) {
    // 移动 DOM 节点回 container
    move(vnode, container, anchor, MoveType.ENTER)
    // 重新 patch (更新 props)
    patch(instance.vnode, vnode, container, anchor, ...)
    // 异步执行 onActivated 钩子
    queuePostRenderEffect(() => {
      instance.isDeactivated = false
      if (instance.a) invokeArrayFns(instance.a) // 'a' -> onActivated
    }, parentSuspense)
  }

  // ... (pruneCacheEntry, onBeforeUnmount 等) ...

  // 5. 【返回自定义 Render 函数】
  //    这个函数是 KeepAlive 的渲染逻辑核心
  return () => {
    // ... (渲染逻辑) ...
  }
}

自定义 render:渲染决策

<KeepAlive> setup 返回的自定义 render 函数是整个系统的控制中心。每当 <KeepAlive> 需要重新渲染时,这个函数就会运行。

它的核心逻辑如下:

typescript
// KeepAlive 的 setup 返回的 render 函数
return () => {
  // 1. 【获取子组件】
  //    获取 <KeepAlive> 插槽中的“当前”子组件 VNode
  const vnode = slots.default && slots.default()[0]
  if (vnode == null) return null // 没有子组件

  const comp = vnode.type as ConcreteComponent
  
  // 2. 【过滤】
  //    检查 props: include / exclude
  const name = getComponentName(comp)
  const { include, exclude, max } = props

  if (
    (include && (!name || !matches(include, name))) || // 不在 include 中
    (exclude && name && matches(exclude, name)) // 在 exclude 中
  ) {
    // 不缓存,将其标记为 current,并直接渲染
    current = vnode 
    return vnode // 返回 VNode,让渲染器按正常流程处理
  }

  // 3. 【生成缓存键】
  //    优先用 VNode 上的 key,否则用组件类型 (comp)
  const key = vnode.key == null ? comp : vnode.key
  
  // 4. 【查找缓存】
  const cachedVNode = cache.get(key)
  
  // --------------------------------------------------
  //  ▼ 关键的渲染决策 ▼
  // --------------------------------------------------

  if (cachedVNode) {
    // 5. 【路径 A:缓存命中 -> 激活】
    
    // a. 复制实例和 DOM
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    
    // b. 标记 VNode:
    //    告诉渲染器,这是一个“被激活”的组件,
    //    不要走“挂载(mount)”流程,而是走“激活(activate)”流程
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    
    // c. 【LRU 优化】
    //    将 key 移动到 Set 的末尾 (标记为“最近使用”)
    keys.delete(key)
    keys.add(key)
    
  } else {
    // 6. 【路径 B:缓存未命中 -> 缓存】
    
    // a. 添加到 LRU 队列
    keys.add(key)

    // b. 【LRU 优化】
    //    检查是否超出 max 限制
    if (max && keys.size > parseInt(max as string, 10)) {
      // 移除“最久未使用”的 key (Set 的第一个元素)
      pruneCacheEntry(keys.values().next().value)
    }

    // c. 标记 VNode:
    //    告诉渲染器,这个组件在“挂载”后,
    //    应该被“缓存”,而不是“销毁”
    vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
  }
  
  // 7. 【交换】
  //    无论如何,都要“去激活”上一个组件
  if (current && current.key !== vnode.key) {
    deactivate(current)
  }
  // 将当前 VNode 设为“活跃”
  current = vnode
  
  // 8. 返回 VNode,让渲染器继续处理
  return vnode
}

ShapeFlags 的作用

  • COMPONENT_SHOULD_KEEP_ALIVE:告诉渲染器,在 patch 之后,调用 <KeepAlive>onMounted/onUpdated 钩子来保存这个 VNode 到 cache 中。
  • COMPONENT_KEPT_ALIVE:告诉渲染器,在 patch 之前跳过 mount,改为调用 <KeepAlive>activate 函数(激活)。

缓存清理:pruneCacheEntry

当缓存数量超出 max 限制时,pruneCacheEntry(修剪缓存条目)会被调用,它负责销毁移除最久未使用的组件。

typescript
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode

  // 1. 如果这个 VNode 不是当前活跃的,就卸载它
  if (!current || !isSameVNodeType(cached, current)) {
    _unmount(cached, instance, parentSuspense, true)
  }

  // 2. 从缓存中删除
  cache.delete(key)
  keys.delete(key)
}

LRU (Least Recently Used) 算法的实现<KeepAlive> 使用 Set 来实现 LRU:

  • keys: Set保持插入顺序
  • keys.values().next().value 永远是最先插入的(即最久未使用的)key。
  • keys.delete(key) + keys.add(key):这个“先删后加”的操作,会把 key 移动到 Set末尾,使其成为“最近使用的”。

组件的生命周期:onActivated / onDeactivated

<KeepAlive> 的子组件可以通过 onActivatedonDeactivated 钩子,感知自己何时被“激活”或“去激活”。

  • setup 中,deactivate(vnode)activate(vnode) 函数会分别调用 instance.da (deactivated 钩子数组) 和 instance.a (activated 钩子数组)。
  • 这两个钩子只在 <KeepAlive> 的子组件中才会被注册和执行。

应用场景

javascript
import { onActivated, onDeactivated, ref } from 'vue'

setup() {
  const timer = ref(null)

  onActivated(() => {
    // 组件被“激活”(从缓存移回 DOM)时
    console.log('组件被激活')
    // 重新开启定时器
    timer.value = setInterval(...)
  })

  onDeactivated(() => {
    // 组件被“去激活”(从 DOM 移入缓存)时
    console.log('组件被去激活')
    // 清理定时器,防止内存泄漏
    clearInterval(timer.value)
  })
}

动态缓存策略:include / exclude

props.includeprops.exclude 提供了对缓存组件的精确控制。

  1. matches 函数: 在 render 函数中,matches 函数会检查组件 name 是否匹配 include/exclude 规则(支持字符串、正则、数组)。

    typescript
    function matches(pattern: MatchPattern, name: string): boolean {
      if (isArray(pattern)) {
        return pattern.some((p: string | RegExp) => matches(p, name))
      } else if (isString(pattern)) {
        return pattern.split(',').includes(name)
      } else if (isRegExp(pattern)) {
        return pattern.test(name)
      }
      return false
    }
  2. watch 动态清理<KeepAlive> 内部有一个 watch 来侦听 include/exclude prop 的变化。如果规则改变了,watch立即遍历现有缓存,将不再符合规则的组件清理掉(调用 pruneCacheEntry)。

    typescript
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        // 遍历 cache,prune 掉不符合新规则的
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      { flush: 'post', deep: true },
    )

总结

<KeepAlive> 是 Vue 渲染器的一个特殊组件。

  1. 它通过自定义 setuprender 函数来接管子组件的渲染。
  2. 它使用 Map 存储 VNode,使用 Set 实现高效的 LRU 策略来管理缓存。
  3. 它通过 ShapeFlags 标记,拦截渲染器的 mount(挂载)和 unmount(卸载)默认行为。
  4. 它调用底层的 move(移动)指令,将组件 DOM 在“渲染容器”和“隐藏的存储容器”之间切换。
  5. 它在 move 的同时,通过 onActivatedonDeactivated 钩子通知子组件其状态的变更。
  6. 它通过 watch 监控 include/exclude动态地清理缓存,并通过 onBeforeUnmount 防止内存泄漏

微信公众号二维码

Last updated: