Skip to content

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

引言

KeepAlive 是 Vue.js 中一个特殊的内置组件,用于缓存组件实例并保持其状态。它通过智能的缓存策略和生命周期管理,避免了组件的重复创建和销毁,从而提升应用性能。本节将深入分析 KeepAlive 的实现原理,包括缓存机制、LRU 算法、生命周期处理等核心特性。

6.1.1 KeepAlive 核心概念

基本用法

KeepAlive 组件通过包裹动态组件来实现缓存:

vue
<template>
  <KeepAlive>
    <component :is="currentComponent" />
  </KeepAlive>
</template>

核心属性

KeepAlive 支持三个核心属性:

typescript
export interface KeepAliveProps {
  include?: MatchPattern  // 只有名称匹配的组件会被缓存
  exclude?: MatchPattern  // 名称匹配的组件不会被缓存
  max?: number | string   // 最多可以缓存多少组件实例
}

type MatchPattern = string | RegExp | (string | RegExp)[]

类型定义

typescript
type CacheKey = PropertyKey | ConcreteComponent
type Cache = Map<CacheKey, VNode>
type Keys = Set<CacheKey>

export interface KeepAliveContext extends ComponentRenderContext {
  renderer: RendererInternals
  activate: (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    namespace: ElementNamespace,
    optimized: boolean,
  ) => void
  deactivate: (vnode: VNode) => void
}

6.1.2 组件缓存机制

缓存数据结构

KeepAlive 使用 Map 和 Set 来管理缓存:

typescript
const cache: Cache = new Map()  // 存储缓存的 VNode
const keys: Keys = new Set()    // 维护缓存键的访问顺序(LRU)
let current: VNode | null = null // 当前活跃的组件

缓存键生成

缓存键的生成逻辑确保了组件的唯一性:

typescript
// 在渲染函数中
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)

缓存键的优先级:

  1. 如果 VNode 有显式的 key 属性,使用该 key
  2. 否则使用组件的构造函数作为 key

缓存命中处理

当缓存命中时,KeepAlive 会复用已缓存的组件实例:

typescript
if (cachedVNode) {
  // 复制已挂载的状态
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  
  // 处理过渡动画
  if (vnode.transition) {
    setTransitionHooks(vnode, vnode.transition!)
  }
  
  // 避免 vnode 被当作新组件挂载
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
  
  // 更新 LRU 顺序:将当前 key 移到最新位置
  keys.delete(key)
  keys.add(key)
} else {
  // 缓存未命中,添加新的缓存项
  keys.add(key)
  
  // 检查是否超出最大缓存数量
  if (max && keys.size > parseInt(max as string, 10)) {
    pruneCacheEntry(keys.values().next().value!)
  }
}

// 标记组件应该被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

缓存存储时机

KeepAlive 在组件挂载和更新后缓存子树:

typescript
let pendingCacheKey: CacheKey | null = null

const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    // 如果 KeepAlive 的子组件是 Suspense,需要等 Suspense 解析后再缓存
    if (isSuspense(instance.subTree.type)) {
      queuePostRenderEffect(() => {
        cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
      }, instance.subTree.suspense)
    } else {
      cache.set(pendingCacheKey, getInnerChild(instance.subTree))
    }
  }
}

onMounted(cacheSubtree)
onUpdated(cacheSubtree)

6.1.3 LRU 算法实现

LRU 原理

KeepAlive 使用 LRU(Least Recently Used)算法来管理缓存,当缓存达到最大数量时,会移除最久未使用的组件。

LRU 数据结构

typescript
// 使用 Set 来维护访问顺序
const keys: Keys = new Set<CacheKey>()

// Set 的特性:
// 1. 插入顺序保持不变
// 2. delete + add 可以将元素移到末尾
// 3. keys.values().next().value 获取最旧的元素

LRU 更新逻辑

typescript
// 缓存命中时,更新 LRU 顺序
if (cachedVNode) {
  // 删除旧位置
  keys.delete(key)
  // 添加到新位置(末尾)
  keys.add(key)
} else {
  // 新增缓存项
  keys.add(key)
  
  // 检查是否超出限制
  if (max && keys.size > parseInt(max as string, 10)) {
    // 移除最久未使用的项(Set 的第一个元素)
    pruneCacheEntry(keys.values().next().value!)
  }
}

缓存清理机制

typescript
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode
  
  // 如果缓存的组件不是当前活跃组件,则卸载它
  if (cached && (!current || !isSameVNodeType(cached, current))) {
    unmount(cached)
  } else if (current) {
    // 当前活跃实例不应该继续被缓存
    // 现在不能卸载它,但可能稍后会卸载,所以重置其标志
    resetShapeFlag(current)
  }
  
  // 从缓存中移除
  cache.delete(key)
  keys.delete(key)
}

function unmount(vnode: VNode) {
  // 重置 shapeFlag 以便正确卸载
  resetShapeFlag(vnode)
  _unmount(vnode, instance, parentSuspense, true)
}

6.1.4 生命周期管理

activated 和 deactivated 钩子

KeepAlive 引入了两个特殊的生命周期钩子:

typescript
export function onActivated(
  hook: Function,
  target?: ComponentInternalInstance | null,
): void {
  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}

export function onDeactivated(
  hook: Function,
  target?: ComponentInternalInstance | null,
): void {
  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}

生命周期钩子注册

typescript
function registerKeepAliveHook(
  hook: Function & { __wdc?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance | null = currentInstance,
) {
  // 缓存去激活分支检查包装器,确保相同的钩子可以被调度器正确去重
  const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () => {
      // 只有当目标实例不在去激活分支中时才触发钩子
      let current: ComponentInternalInstance | null = target
      while (current) {
        if (current.isDeactivated) {
          return
        }
        current = current.parent
      }
      return hook()
    })
  
  injectHook(type, wrappedHook, target)
  
  // 除了在目标实例上注册,还要向上遍历父链
  // 在所有作为 keep-alive 根的祖先实例上注册
  if (target) {
    let current = target.parent
    while (current && current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
        injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
  }
}

激活和去激活处理

typescript
// 激活组件
sharedContext.activate = (
  vnode,
  container,
  anchor,
  namespace,
  optimized,
) => {
  const instance = vnode.component!
  
  // 移动 DOM 节点到目标容器
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  
  // 更新 props(可能已更改)
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    namespace,
    vnode.slotScopeIds,
    optimized,
  )
  
  // 异步执行激活钩子
  queuePostRenderEffect(() => {
    instance.isDeactivated = false
    if (instance.a) {
      invokeArrayFns(instance.a) // 调用 activated 钩子
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeMounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
  }, parentSuspense)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    devtoolsComponentAdded(instance)
  }
}

// 去激活组件
sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!
  
  // 取消挂载和激活的副作用
  invalidateMount(instance.m)
  invalidateMount(instance.a)

  // 移动 DOM 节点到存储容器
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  
  // 异步执行去激活钩子
  queuePostRenderEffect(() => {
    if (instance.da) {
      invokeArrayFns(instance.da) // 调用 deactivated 钩子
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    instance.isDeactivated = true
  }, parentSuspense)

  if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
    devtoolsComponentAdded(instance)
  }
}

6.1.5 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)) {
    pattern.lastIndex = 0
    return pattern.test(name)
  }
  return false
}

过滤逻辑

typescript
// 在渲染函数中进行过滤
const name = getComponentName(
  isAsyncWrapper(vnode)
    ? (vnode.type as ComponentOptions).__asyncResolved || {}
    : comp,
)

const { include, exclude, max } = props

// 检查是否应该缓存
if (
  (include && (!name || !matches(include, name))) ||
  (exclude && name && matches(exclude, name))
) {
  // 不缓存,移除 SHOULD_KEEP_ALIVE 标志
  vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
  current = vnode
  return rawVNode
}

动态过滤更新

typescript
// 监听 include/exclude 属性变化
watch(
  () => [props.include, props.exclude],
  ([include, exclude]) => {
    include && pruneCache(name => matches(include, name))
    exclude && pruneCache(name => !matches(exclude, name))
  },
  // 在渲染后修剪,确保 current 已更新
  { flush: 'post', deep: true },
)

function pruneCache(filter: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent)
    if (name && !filter(name)) {
      pruneCacheEntry(key)
    }
  })
}

6.1.6 DOM 节点管理

存储容器

KeepAlive 创建一个隐藏的存储容器来保存去激活的组件:

typescript
const storageContainer = createElement('div')

DOM 移动策略

typescript
// 激活时:从存储容器移动到目标容器
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)

// 去激活时:从目标容器移动到存储容器
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)

ShapeFlag 管理

typescript
function resetShapeFlag(vnode: VNode) {
  // 使用位运算移除 keep-alive 标志
  vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
  vnode.shapeFlag &= ~ShapeFlags.COMPONENT_KEPT_ALIVE
}

// 标记组件应该被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

// 标记组件已被缓存
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE

6.1.7 内存管理与性能优化

内存泄漏防护

typescript
// 组件卸载时清理所有缓存
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    const vnode = getInnerChild(subTree)
    
    if (cached.type === vnode.type && cached.key === vnode.key) {
      // 当前实例将作为 keep-alive 卸载的一部分被卸载
      resetShapeFlag(vnode)
      // 但在这里调用其 deactivated 钩子
      const da = vnode.component!.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    unmount(cached)
  })
})

缓存大小控制

typescript
// 通过 max 属性控制最大缓存数量
if (max && keys.size > parseInt(max as string, 10)) {
  pruneCacheEntry(keys.values().next().value!)
}

异步组件处理

typescript
// 获取内部子组件(处理 Suspense 包装)
function getInnerChild(vnode: VNode) {
  return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode
}

// 处理异步组件的名称获取
const name = getComponentName(
  isAsyncWrapper(vnode)
    ? (vnode.type as ComponentOptions).__asyncResolved || {}
    : comp,
)

6.1.8 实际应用场景

路由缓存

vue
<template>
  <router-view v-slot="{ Component }">
    <KeepAlive :include="cachedViews">
      <component :is="Component" :key="$route.fullPath" />
    </KeepAlive>
  </router-view>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const cachedViews = ref(['Home', 'Profile', 'Settings'])

// 动态控制缓存
const shouldCache = computed(() => {
  return route.meta?.keepAlive !== false
})
</script>

标签页缓存

vue
<template>
  <div class="tabs">
    <div class="tab-headers">
      <button
        v-for="tab in tabs"
        :key="tab.id"
        :class="{ active: currentTab === tab.id }"
        @click="currentTab = tab.id"
      >
        {{ tab.title }}
        <span @click.stop="closeTab(tab.id)">×</span>
      </button>
    </div>
    
    <KeepAlive :include="cachedTabs">
      <component
        :is="currentTabComponent"
        :key="currentTab"
      />
    </KeepAlive>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const tabs = ref([
  { id: 'tab1', title: 'Tab 1', component: 'TabContent1' },
  { id: 'tab2', title: 'Tab 2', component: 'TabContent2' },
  { id: 'tab3', title: 'Tab 3', component: 'TabContent3' },
])

const currentTab = ref('tab1')
const cachedTabs = ref(['TabContent1', 'TabContent2'])

const currentTabComponent = computed(() => {
  const tab = tabs.value.find(t => t.id === currentTab.value)
  return tab?.component
})

function closeTab(tabId) {
  const index = tabs.value.findIndex(t => t.id === tabId)
  if (index > -1) {
    tabs.value.splice(index, 1)
    // 从缓存中移除
    const component = tabs.value[index]?.component
    if (component) {
      const cacheIndex = cachedTabs.value.indexOf(component)
      if (cacheIndex > -1) {
        cachedTabs.value.splice(cacheIndex, 1)
      }
    }
  }
}
</script>

表单状态保持

vue
<template>
  <KeepAlive>
    <FormComponent
      v-if="showForm"
      @submit="handleSubmit"
      @cancel="showForm = false"
    />
  </KeepAlive>
</template>

<script setup>
import { ref, onActivated, onDeactivated } from 'vue'

const showForm = ref(true)

// 在组件内部使用生命周期钩子
const FormComponent = {
  setup() {
    const formData = ref({
      name: '',
      email: '',
      message: ''
    })
    
    onActivated(() => {
      console.log('表单组件被激活')
      // 可以在这里恢复焦点、刷新数据等
    })
    
    onDeactivated(() => {
      console.log('表单组件被去激活')
      // 可以在这里保存草稿、清理定时器等
    })
    
    return { formData }
  }
}
</script>

6.1.9 性能优化策略

缓存策略优化

typescript
// 1. 合理设置 max 值
<KeepAlive :max="10">
  <component :is="currentComponent" />
</KeepAlive>

// 2. 精确控制 include/exclude
<KeepAlive :include="/^(Home|Profile)$/">
  <router-view />
</KeepAlive>

// 3. 动态调整缓存策略
const cacheStrategy = computed(() => {
  if (isLowMemoryDevice()) {
    return { max: 3, include: ['Home'] }
  }
  return { max: 10, include: cachedComponents.value }
})

内存监控

typescript
// 监控缓存使用情况
function useCacheMonitor() {
  const cacheSize = ref(0)
  const memoryUsage = ref(0)
  
  const updateStats = () => {
    // 获取缓存大小
    const keepAliveInstance = getCurrentInstance()
    if (keepAliveInstance?.__v_cache) {
      cacheSize.value = keepAliveInstance.__v_cache.size
    }
    
    // 监控内存使用(仅在支持的浏览器中)
    if (performance.memory) {
      memoryUsage.value = performance.memory.usedJSHeapSize
    }
  }
  
  onMounted(() => {
    const timer = setInterval(updateStats, 5000)
    onUnmounted(() => clearInterval(timer))
  })
  
  return { cacheSize, memoryUsage }
}

预加载策略

typescript
// 预加载关键组件
function usePreloadStrategy() {
  const preloadComponent = async (componentName: string) => {
    try {
      const component = await import(`@/components/${componentName}.vue`)
      // 预创建组件实例但不挂载
      return component.default
    } catch (error) {
      console.warn(`Failed to preload component: ${componentName}`, error)
    }
  }
  
  onMounted(() => {
    // 预加载常用组件
    const commonComponents = ['UserProfile', 'Settings', 'Dashboard']
    commonComponents.forEach(preloadComponent)
  })
}

6.1.10 调试与开发工具

开发时调试

typescript
// 开发环境下的缓存状态监控
if (__DEV__) {
  const instance = getCurrentInstance()
  if (instance) {
    // 暴露缓存实例供调试
    ;(instance as any).__v_cache = cache
    
    // 添加调试信息
    window.__VUE_KEEPALIVE_CACHE__ = {
      cache,
      keys,
      current,
      stats: {
        size: () => cache.size,
        keys: () => Array.from(keys),
        clear: () => {
          cache.clear()
          keys.clear()
        }
      }
    }
  }
}

DevTools 集成

typescript
// DevTools 组件树更新
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  devtoolsComponentAdded(instance)
}

总结

KeepAlive 组件通过精心设计的缓存机制和生命周期管理,为 Vue.js 应用提供了强大的组件状态保持能力。其核心特性包括:

关键特性

  1. 智能缓存:基于 Map 和 Set 的高效缓存数据结构
  2. LRU 算法:自动管理缓存大小,移除最久未使用的组件
  3. 生命周期扩展:提供 activated/deactivated 钩子
  4. 灵活过滤:支持 include/exclude 模式匹配
  5. DOM 优化:通过 DOM 移动而非重新创建来提升性能

性能优势

  1. 避免重复渲染:缓存组件实例和 DOM 状态
  2. 保持用户状态:表单数据、滚动位置等状态得以保留
  3. 减少网络请求:避免重复的数据获取
  4. 优化用户体验:快速的页面切换和状态恢复

最佳实践

  1. 合理设置缓存大小:根据应用需求和设备性能调整 max 值
  2. 精确控制缓存范围:使用 include/exclude 避免不必要的缓存
  3. 监控内存使用:在生产环境中监控缓存对内存的影响
  4. 处理副作用:在 activated/deactivated 钩子中正确处理定时器、事件监听器等

KeepAlive 的实现展现了 Vue.js 在性能优化方面的深度思考,为开发者提供了一个既强大又易用的组件缓存解决方案。


微信公众号二维码