Code前端首页关于Code前端联系我们

一、先搞懂Vue3的keep alive是干啥的?

terry 12小时前 阅读数 13 #Vue
文章标签 alive

p>在Vue3项目开发里,组件缓存是优化性能的常用手段,而keep - alive就是实现缓存的核心工具,但很多同学刚接触时,会发现组件的生命周期和没加keep - alive时完全不一样——明明退出组件了,unmounted钩子没触发;再次进入组件,mounted也不执行了…这到底咋回事?今天就把Vue3中keep - alive的生命周期逻辑、实际开发咋用这些问题,掰开了揉碎了讲清楚。 keep - alive是Vue3的内置组件,作用是缓存组件实例,举个日常开发里的例子:做tab切换功能时,切换到A标签页,组件A里的表单填了一半;切到B标签页再切回来,如果没keep - alive,组件A会被销毁重建,表单内容就没了;但用了keep - alive,组件A的实例会被“存起来”,切回来时直接复用,表单状态、定时器这些都能保留。

它实现缓存的逻辑也不复杂:keep - alive内部维护了一个缓存对象,当包裹的组件第一次渲染时,把组件实例存到缓存里;之后再触发渲染时,优先从缓存里取实例,而不是重新创建,这就跳过了组件的“创建(created)→挂载(mounted)”这些初始化流程,直接复用现成的实例,性能自然就上去了。

Vue3中keep - alive涉及的生命周期钩子有啥不同?

在没被keep - alive包裹时,组件的生命周期是创建(created)→挂载(mounted)→更新(updated)→销毁(unmounted) 这套流程,但被keep - alive包裹后,生命周期会变成“创建/挂载只执行一次 + 激活/失活反复切换” 的逻辑,还新增了两个专属钩子:onActivatedonDeactivated

咱分场景拆解开看:

组件第一次进入(缓存创建阶段)

执行顺序是createdmountedonActivated,这时候组件完成初始化,进入“激活”状态,能正常渲染UI、绑定事件。
举个实际开发的例子:做一个「用户信息编辑页」,第一次进入时,created里发请求拿用户数据,mounted里初始化富文本编辑器,onActivated里可以把页面滚动到顶部(因为是第一次激活,也能做这类初始化操作)。

组件离开(进入缓存阶段)

这时候不会执行unmounted!因为组件没被销毁,只是“暂时藏起来”,执行的是onDeactivated
还是刚才的编辑页,离开时(比如切到其他tab),onDeactivated里可以暂停富文本编辑器的自动保存定时器,避免切走后还在偷偷发请求;也能把当前表单的临时数据存到全局状态里,方便下次回来复用。

组件再次进入(缓存复用阶段)

这时候跳过createdmounted,直接执行onActivated,因为组件实例早就存在,不需要重新初始化。
回到编辑页时,onActivated里重启自动保存定时器,把之前存在全局的临时数据再赋值回表单,用户体验就会很流畅,感觉页面从没离开过。

为了让大家更直观理解,贴个组合式API的代码示例:

<template>
  <div>{{ count }}</div>
  <button @click="count++">+</button>
</template>
<script setup>
import { ref, onCreated, onMounted, onActivated, onDeactivated } from 'vue'
const count = ref(0)
onCreated(() => {
  console.log('created——只在第一次进入执行')
})
onMounted(() => {
  console.log('mounted——只在第一次进入执行')
})
onActivated(() => {
  console.log('activated——每次进入(包括复用)执行')
})
onDeactivated(() => {
  console.log('deactivated——每次离开执行')
})
</script>

把这个组件用<keep - alive><MyComponent /></keep - alive>包裹后,切换组件时看控制台:

  • 第一次进入:createdmountedactivated
  • 离开:deactivated
  • 再次进入:activatedcreatedmounted不执行了)
  • 完全销毁组件(比如把keep - alive去掉,再切换):才会执行unmounted

这就能明白:keep - alive让组件在“激活↔失活”间切换,而不是“创建↔销毁”,所以生命周期钩子的触发逻辑完全变了。

onActivated和onDeactivated里能做啥操作?

这两个钩子是专门为keep - alive场景设计的,开发中常用这几个方向:

数据层面:刷新或保留状态

  • 「保留状态」场景:比如多步骤表单、tab内的临时编辑内容,在onDeactivated把临时数据存到Pinia/Vuex,onActivated再读回来。
  • 「强制刷新」场景:比如新闻列表页,希望每次切回来都加载最新新闻,这时候在onActivated里发请求,覆盖旧数据。

副作用管理:定时器、事件监听

组件没被销毁,只是失活,所以定时器、全局事件(比如window.resize)如果不处理,会一直运行,浪费性能甚至报错。

举个定时器的例子:

let timer = null
onActivated(() => {
  timer = setInterval(() => { console.log('自动保存') }, 3000)
})
onDeactivated(() => {
  clearInterval(timer)
  timer = null
})

onActivated里重启定时器、绑定事件;onDeactivated里清定时器、解绑事件,保证组件激活时功能正常,失活时暂停。

DOM操作:恢复交互状态

因为组件实例被缓存,DOM也没被销毁,只是样式可能被隐藏了。onActivated里可以做DOM相关操作:

  • 表单组件切回来时,把输入框聚焦(用nextTick确保DOM更新);
  • 富文本编辑器切回来时,恢复编辑状态;
  • 列表组件切回来时,滚动到上次浏览的位置(需要在onDeactivated记录scrollToponActivated时设置回去)。

再举个电商场景的例子:
用户逛商品列表页(A),点进商品详情页(B,被keep - alive缓存),在B页里,用户把商品加入购物车,还填了留言,这时切回A页,B页触发onDeactivated,可以把「留言内容」存到全局;等再切回B页,onActivated再赋值到输入框,同时检查购物车状态是否更新(可能在onActivated里发个请求刷新购物车标记)。

反过来,如果是个「搜索结果页」,需要每次进入都重新搜索,就不能依赖缓存保留状态,这时候要么不用keep - alive,要么在onActivated里强制清空搜索参数、重新发请求。

使用keep - alive时容易踩的坑有哪些?

虽然keep - alive能提升性能,但用不对也会出问题,这几个坑要避开:

生命周期钩子混淆,导致资源泄漏

很多同学习惯在onUnmounted里清定时器、解绑事件,但被keep - alive包裹后,组件离开时执行的是onDeactivated,不是onUnmounted!如果把清理逻辑只写在onUnmounted里,定时器会一直跑,越积越多,页面越来越卡。

真实案例:之前带团队做项目,有个同学做「实时聊天窗口」组件,用了keep - alive缓存,离开聊天页时,他把WebSocket断开逻辑写在onUnmounted里,结果切到其他页面后,WebSocket还在发消息,后台一直收到心跳包,最后用户被强制下线才发现问题,后来把断开逻辑移到onDeactivated里,问题就解决了。

解决方法:把「组件失活时要清理的逻辑」放到onDeactivated,「组件真正销毁时(比如路由跳转后永远不回来)」的清理逻辑才放onUnmounted

include/exclude配置错误,缓存不符合预期

keep - alive有include(只缓存匹配的组件)和exclude(不缓存匹配的组件)属性,值是组件的name(注意是组件选项里的name,不是标签名),如果配置错了,会出现:

  • 想缓存的组件没被缓存(比如name写漏了、大小写不对);
  • 不想缓存的组件被缓存了(比如exclude里没写对组件name)。

错误案例:组件定义时name是GoodsDetail,但keep - alive的include写的是goodsDetail(小写),就匹配不上,导致缓存失效。

解决方法:统一组件name的命名规范(PascalCase),配置include/exclude时严格对应。

多实例组件缓存冲突(动态组件场景)

比如用<component :is="currentComp" />配合keep - alive做动态组件切换,多个组件共享同一个缓存池,容易出现“数据串了”的问题。

例子:做tab切换,TabA和TabB用同一个动态组件(比如都是FormComponent),但各自要填不同数据,如果用keep - alive缓存FormComponent,切tab时会发现上一个tab的表单数据跑到当前tab了。

解决方法

  • 给动态组件加不同的key,让keep - alive认为是不同实例(但这样缓存意义就弱了);
  • 不在组件级别缓存,而是在onActivated里根据路由/参数重置数据;
  • 拆分组件,让每个tab用独立的组件,避免复用同一个组件导致状态混淆。

路由缓存和页面刷新的矛盾

在路由外层包keep - alive后,页面刷新(F5)会导致缓存失效(因为页面重载,内存被清空),这时候用户切回页面,会发现之前缓存的状态没了。

解决方法

  • 重要状态(比如表单草稿)持久化到 localStorage/sessionStorage,在onActivated时优先读缓存;
  • 区分“路由切换导致的组件失活”和“页面刷新导致的组件销毁”,在onMounted里也做一次状态恢复(因为页面刷新后组件会重新mounted)。

结合路由场景,keep - alive咋和路由配合?

实际项目里,经常需要对路由组件做缓存(比如首页、列表页切换时保留状态),这时候要把<router - view>包在<keep - alive>里,再结合路由元信息(meta)控制缓存逻辑。

基础用法:全局路由组件缓存

在App.vue里这样写:

<template>
  <keep - alive>
    <router - view></router - view>
  </keep - alive>
</template>

这样所有路由组件都会被缓存,但实际项目里,往往只有部分页面需要缓存(我的收藏」页需要保留状态,「登录页」不需要缓存),这时候得用include或路由meta控制。

用meta控制是否缓存

在路由配置里给需要缓存的页面加meta:

const routes = [
  {
    path: '/favorites',
    name: 'Favorites',
    component: () => import('./views/Favorites.vue'),
    meta: { keepAlive: true } // 需要缓存
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('./views/Login.vue'),
    meta: { keepAlive: false } // 不需要缓存
  }
]

然后在App.vue里动态控制keep - alive的包裹:

<template>
  <keep - alive v - if="$route.meta.keepAlive">
    <router - view></router - view>
  </keep - alive>
  <router - view v - else></router - view>
</template>

但这样有个问题:如果当前页面是缓存的,跳转到新页面再回来,页面会重新渲染(因为v - if切换导致keep - alive销毁重建)。更优雅的方式是用include动态计算要缓存的组件name

<template>
  <keep - alive :include="keepAliveComponents">
    <router - view></router - view>
  </keep - alive>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue - router'
const route = useRoute()
const keepAliveComponents = computed(() => {
  // 只缓存meta.keepAlive为true的组件,且组件name要和路由组件的name一致
  return route.meta.keepAlive ? [route.name] : []
})
</script>

这里要注意:路由的name要和组件的name选项一致,否则include匹配不到。

路由切换时的生命周期触发

当用keep - alive包裹路由组件后,路由切换时的钩子执行逻辑:

  • 从页面A(缓存)跳转到页面B(首次进入):A执行onDeactivated,B执行createdmountedonActivated
  • 从页面B跳回页面A:B执行onDeactivated,A执行onActivated(因为A是缓存组件,复用实例);
  • 跳转到一个不缓存的页面C,再跳回A:如果A的meta.keepAlive为true,A还是执行onActivated,因为缓存还在。

这种逻辑下,做「页面返回时保留滚动位置」「表单草稿自动保存」这类需求就很顺:在onDeactivated存状态,onActivated恢复状态。

和Vue2比,Vue3的keep - alive生命周期有啥进化?

用过Vue2的同学会发现,Vue3在keep - alive的生命周期设计上更贴合现代前端开发习惯,主要有这些变化:

组合式API的钩子更简洁

Vue2里用选项式API,activated和deactivated是组件选项里的函数:

export default {
  activated() { ... },
  deactivated() { ... }
}

Vue3的组合式API用onActivatedonDeactivated,可以和onMountedonUnmounted等钩子在setup里平级使用,逻辑拆分更灵活,比如在一个自定义hook里封装定时器逻辑:

export function useAutoSave() {
  let timer = null
  onActivated(() => {
    timer = setInterval(save, 5000)
  })
  onDeactivated(() => {
    clearInterval(timer)
  })
  onUnmounted(() => {
    clearInterval(timer) // 保险措施,防止页面刷新没触发deactivated
  })
}

在组件里只要useAutoSave(),逻辑就自动注入,比Vue2的mixin或选项式写法更清晰。

对复杂组件结构的支持更好

Vue3支持Fragment(组件可以有多个根节点),keep - alive在处理这类组件时更高效,不会因为根节点数量问题导致缓存异常,而Vue2里如果组件有多个根节点,keep - alive可能出现渲染错误,需要额外处理。

钩子执行时机的一致性优化

Vue2里,activated钩子在mounted之后执行,但如果组件里有异步组件或动态组件,钩子顺序可能不稳定,Vue3的组合式API通过响应式依赖追踪和setup的执行时机,让onActivated等钩子的触发更可预测,减少了因异步逻辑导致的钩子顺序bug。

举个Vue2的坑:如果在activated里访问DOM,可能因为组件还没完全激活导致获取不到;Vue3的onActivated在组件DOM更新后触发(类似onMounted的nextTick逻辑),所以能安全操作DOM。

掌握逻辑,平衡性能与体验

Vue3的keep - alive通过缓存组件实例,让组件在「激活↔失活」间切换,对应的onActivatedonDeactivated钩子是开发的核心抓手,理解这套生命周期逻辑后,才能在性能优化(减少组件重复渲染)和用户体验(保留状态、恢复交互)之间找到平衡。

实际开发中,要记住这几点:

  • 被keep - alive包裹的组件,销毁逻辑要放到onDeactivated,真正的销毁(页面刷新、路由跳转后不返回)才用onUnmounted
  • 利用include/exclude精准控制缓存范围,别让不该缓存的组件占内存;
  • 结合路由meta和组合式API,把状态管理、副作用处理拆分到onActivatedonDeactivated里,让代码更整洁;
  • 遇到缓存冲突、状态残留这些坑,从「组件name匹配」「动态key」「状态重置时机」这几个方向排查。

现在再回头看开头的问题——“Vue3里keep - alive的生命周期咋变化?实际开发咋用?”——是不是清晰多了?下次遇到组件缓存相关的需求,就知道从哪些角度设计逻辑啦~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门