Skip to content

06|KeepAlive ​

第七章 / 06。目标:分清“实例仍在缓存”“当前在 DOM 中”“业务数据仍新鲜”三件事。

生命周期从二态变成三态 ​

普通动态组件切走时会卸载;被 <KeepAlive> 包裹后会进入 deactivated,实例、响应式状态和渲染子树仍保留。再次显示时进入 activated,不重新创建实例。

text
首次显示:setup → mounted → activated
切走:deactivated
切回:activated
已停用实例被 LRU 淘汰:unmounted
当前活动实例随 KeepAlive 卸载:deactivated → unmounted

onActivated 首次挂载也会调用;onDeactivated 在最终卸载时也会调用。只在 onUnmounted 暂停轮询,会让已停用页面继续消耗资源。

可重放实验:缓存状态与外部资源 ​

把下列子组件保存为 src/CachePane.vue:

vue
<script setup lang="ts">
import { onActivated, onDeactivated, onMounted, onUnmounted, ref } from 'vue'
const props = defineProps<{ name: string }>()
const emit = defineEmits<{ log: [message: string] }>()
const draft = ref('')
const polls = ref(0)
let timer: ReturnType<typeof setInterval> | undefined
const log = (event: string) => emit('log', `${props.name}:${event}`)
function stop() { clearInterval(timer); timer = undefined }
onMounted(() => log('mounted'))
onActivated(() => {
  log('activated')
  stop()
  timer = setInterval(() => { polls.value++; log('poll') }, 1000)
})
onDeactivated(() => { stop(); log('deactivated') })
onUnmounted(() => { stop(); log('unmounted') })
</script>
<template>
  <section>
    <h2>{{ name }}</h2>
    <label>草稿 <input v-model="draft"></label>
    <output>计数:{{ polls }}</output>
  </section>
</template>

父组件 src/App.vue:

vue
<script setup lang="ts">
import { ref } from 'vue'
import CachePane from './CachePane.vue'
const active = ref('A')
const trace = ref<string[]>([])
</script>
<template>
  <nav><button v-for="name in ['A', 'B', 'C']" :key="name" @click="active = name">{{ name }}</button></nav>
  <KeepAlive :max="2">
    <CachePane :key="active" :name="active" @log="trace.push($event)" />
  </KeepAlive>
  <pre>{{ trace.join('\n') }}</pre>
</template>

运行 npm run dev。在 A 输入草稿,等计数变化后切到 B,记住 A 的最后计数;切回 A 时应保留草稿,停用期间没有 A:poll。日志放在父级响应式数组中,即使 A 被移动到缓存容器或卸载,历史仍能看到。普通 string[] 的 push 不触发界面更新,不适合这个观察目标。

KeepAlive 复用的是组件实例及其渲染子树,停用把 DOM 移到内部存储容器,而非终止整个实例;所以副作用要在停用钩子中暂停。max=2 时依次 A→B→C,最近使用顺序从 [A] 到 [A,B] 再到 [B,C];A 在 B 显示时已发生 deactivated,淘汰时再发生 unmounted,不要求同一时刻重复发 deactivated。最后回到 A 会新建实例。这里用同一组件加不同 key 构成三个缓存身份;max 计的是身份数,不是组件类型数或内存字节。

include、key 与数据新鲜度 ​

include/exclude 按组件 name 匹配;<script setup> 的 SFC 可从文件名推断 name,但包装组件和路由结构仍应实际验证。不同 key 会形成不同缓存身份,同一路由参数是否复用应由产品语义决定。

缓存不等于数据缓存策略。可在激活时根据时间戳或版本检查 freshness:

ts
const loadedAt = ref(0)

onActivated(async () => {
  if (Date.now() - loadedAt.value < 30_000) return
  await refresh()
  loadedAt.value = Date.now()
})

30 秒只是示例输入,不是通用正确值。敏感草稿在登出、租户切换或权限下降时应显式清除;不能等 LRU 偶然淘汰。

练习:验证停用与淘汰 ​

制作 A/B/C 三个缓存身份(可用同一组件加不同 key),A 有草稿和每秒计数器,父级 KeepAlive max=2。合同:A→B→A 后草稿保留且停用期间计数器不增长;A→B→C→A 后 A 已被淘汰并恢复初始草稿。记录生命周期 trace,首次学习先按上述按钮顺序记录日志;自动化回访题在完成 测试环境 与 组合式函数测试 后,用假计时器复核相同合同。

参考答案:第 7 章第 06 题。

用心记录每一步成长