先记住这个答案
被 KeepAlive 缓存的组件切走时进入 deactivated,不会像普通动态组件那样立即 unmounted;切回来时 activated,原实例与本地状态继续使用。onActivated 也会在首次挂载时调用,onDeactivated 也会在最终卸载时调用。只在可见期需要的轮询、媒体或快捷键可在 activated 启动、deactivated 停止;必须保留的缓存状态不要在失活时清空。最终资源关闭仍应保证幂等,并考虑缓存 max 淘汰。
- 失活表示缓存而非实例销毁
- activated 包含首次挂载,deactivated 包含最终卸载
- 按可见期暂停资源,按实例寿命保存状态
普通挂载卸载不足以描述缓存切换
没有 KeepAlive 时,动态组件切走会卸载,切回创建新实例。缓存后目标正是保留输入、滚动或局部计算结果,所以切换过程不能继续按销毁和重建处理。
如果轮询只在组件可见时有意义,却只在 unmounted 才停止,它会在缓存期间持续请求。反过来,如果失活时把表单状态全部重置,又违背了使用缓存保留状态的目的。
让可见期任务具有幂等 start 和 stop
下例在 activated 中增加活动标记,在 deactivated 中移除。真实任务可以替换为事件订阅或播放控制,但 start 必须避免重复注册,stop 也要允许最终卸载路径再次调用。
事件记录可以验证首次挂载会出现 activated,切到另一个组件会出现 deactivated,切回又出现 activated。不要只期待第二次进入缓存时才调用激活钩子。
import { h, onActivated, onDeactivated, ref } from 'vue'
const keepAliveEvents: string[] = []
const active = ref(false)
const CachedPanel = {
name: 'CachedPanel',
setup() {
onActivated(() => {
active.value = true
keepAliveEvents.push('activated')
})
onDeactivated(() => {
active.value = false
keepAliveEvents.push('deactivated')
})
return () => h('span', active.value ? 'active' : 'inactive')
}
}将 CachedPanel 放入 KeepAlive 的动态分支后,首次显示记录 activated,切走记录 deactivated,切回再记录 activated。active 是演示状态,真实组件应避免钩子写入造成无意义的额外渲染。
缓存淘汰仍会走最终结束
KeepAlive 的 max 类似最近使用缓存限制,容量超出时旧实例会被销毁。应用不能假定进入缓存后永远存在,关键草稿应有更持久的保存策略,资源也要能在最终结束时释放。
某项共享订阅若同时服务多个缓存组件,应交给独立服务和引用计数,不能由任意一个组件 deactivated 就整体关闭。可见期订阅与跨组件共享服务的生命周期需要分层。
容易答错的地方
- 只写 mounted 和 unmounted 控制轮询
- 缓存切走不一定卸载,轮询会继续;切回也不会再次 mounted,无法重新启动。可见期工作应使用 activated 与 deactivated,永久资源再按组件结束处理。
- 在 deactivated 中清空所有本地状态
- KeepAlive 的主要价值就是保留实例状态。只暂停与不可见相关的副作用,是否重置数据应由产品语义决定,不能把失活误当成完整销毁。
面试官还会怎么问?
onDeactivated 在最终卸载时也会调用吗?
会。缓存实例最终卸载时也会进入该钩子,因此停止函数应幂等,避免与 onUnmounted 的另一条清理重复调用同一资源后抛错。
后代组件也能收到这些钩子吗?
缓存根组件以及其缓存树中的后代都可以注册激活与失活钩子。若多层操作同一共享资源,应明确唯一 owner,防止重复启动和停止。
缓存组件中的 watch 会自动暂停吗?
不能假定所有响应式效果因 deactivated 自动停止。实例仍被保留,需要根据业务在失活钩子中暂停昂贵任务,或让 watch 自己检查可见状态并正确清理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。