06|KeepAlive
第七章 / 06。目标:分清“实例仍在缓存”“当前在 DOM 中”“业务数据仍新鲜”三件事。
生命周期从二态变成三态
普通动态组件切走时会卸载;被 <KeepAlive> 包裹后会进入 deactivated,实例、响应式状态和渲染子树仍保留。再次显示时进入 activated,不重新创建实例。
首次显示:setup → mounted → activated
切走:deactivated
切回:activated
已停用实例被 LRU 淘汰:unmounted
当前活动实例随 KeepAlive 卸载:deactivated → unmountedonActivated 首次挂载也会调用;onDeactivated 在最终卸载时也会调用。只在 onUnmounted 暂停轮询,会让已停用页面继续消耗资源。
可重放实验:缓存状态与外部资源
把下列子组件保存为 src/CachePane.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:
<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:
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 题。