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

Vue3 Keep-Alive 为什么总不生效?帮你排查90%踩坑场景

terry 1小时前 阅读数 23 #Vue
文章标签 AliveAlive踩坑

最近刚把项目从Vue2迁移到Vue3,写完列表页跳详情再返回,发现列表页居然重新加载了一遍,翻到的页码和选中的筛选全没了——这不就是我特意加的Keep-Alive没起作用嘛?踩坑踩得直挠头,翻了不少开发文档,还在社区问了朋友,最后终于把常见情况都摸清楚了,今天就好好整理一下,不管是用Options API还是Composition API的setup,不管是Router切换还是组件嵌套,看完这篇基本能解决问题。

别先急着改代码,先确认Keep-Alive的基础条件满足了吗?

很多人一上来就怀疑是不是Vue3变了规则,其实大部分时候都是基础的东西没做对,Keep-Alive要生效,不管Vue2还是Vue3,核心前提是组件必须被缓存机制“识别”并“包裹”,而且不能有强制让组件卸载的操作,先对照着检查这几点:

包裹方式对不对?

Vue3的Keep-Alive用法和Vue2略有不同,但包裹的结构是关键,不能直接把Keep-Alive包在router-view外面就完事儿,很多时候我们会用v-if/v-show切换路由视图(比如切换登录页和主内容区),如果这时候Keep-Alive的位置放错了,就会跟着被销毁,正确的做法是:如果只需要缓存主内容区的路由,Keep-Alive要放在v-if对应的那个块里,只包裹当前需要缓存的动态视图。

还有,不管是用Router的include/exclude/max属性,还是直接包裹所有组件,Keep-Alive的子元素必须是单个的动态组件或者router-view,如果中间夹了div、section这种普通标签,缓存机制就失效了,比如以前有人写<keep-alive><div><router-view /></div></keep-alive>,这样是不行的,要把普通标签去掉,或者用<component :is="..." />替代router-view同时处理标签包裹的需求?不对,替代也得保证component是Keep-Alive的直接子元素,普通标签得放在动态组件的内部,比如组件自身的template里加div。

include/exclude/max的匹配逻辑搞懂了吗?

很多人会用这三个属性来控制缓存范围,但匹配错了也是常见问题。匹配的是组件的name属性,不是路由的name!不是路由的path! 这个坑Vue2也有,但Vue3如果用setup语法糖的话,默认组件是没有name的,必须显式声明——要么加<script setup name="ListPage"></script>,要么在同一个文件里加一个普通的<script>标签export default { name: 'ListPage' },显式声明之后,include里写['ListPage']才有用,不然肯定匹配不上。

exclude和max同理,max是缓存组件的最大数量,超过的话会把最早缓存的组件踢出去(LRU算法),如果设置成0或者负数,缓存就直接关闭了,还要注意匹配的大小写,组件的name和include/exclude里的字符串必须完全一致,比如组件叫UserList,include里写userlist或者USER_LIST都不行。

有没有用v-if把组件/路由强制干掉了?

刚才说过v-if和Keep-Alive位置的问题,还有一种情况是在组件内部或者路由元信息里用v-if控制整个包裹块,比如有人在主内容区写了<keep-alive include="ListPage"><router-view v-if="isLogin" /></keep-alive>,然后isLogin因为某种原因短暂变成false又变成true——这时候整个router-view都会被销毁重建,Keep-Alive缓存的内容也会跟着丢,不管ListPage有没有在include里,这时候应该用v-show代替v-if,v-show只是控制组件的显示隐藏,不会销毁DOM和组件实例,缓存就能保留下来。

如果基础条件都满足,那大概率是Vue3特有的坑了?

基础的都检查完,还是不生效?那就要看看是不是Vue3独有的语法或者API导致的,比如setup语法糖、Teleport、Suspense这些新东西和Keep-Alive的兼容性问题。

setup语法糖下,组件的生命周期钩子有没有用对?

很多人用setup语法糖后,还会用以前Vue2的mounted、destroyed,但Vue3里对应的是onMounted、onUnmounted,而且Keep-Alive激活/失活的钩子也变了:Vue2是activated和deactivated,Vue3的Options API还是这两个,但setup语法糖里要写成onActivatedonDeactivated,而且必须显式从vue里导入!比如忘记导入onActivated,直接写onActivated(() => {})是会报错的,但有时候没报错(比如拼错了或者其他原因),就会觉得Keep-Alive没生效,其实组件已经被缓存了,只是你没在激活时去恢复数据而已——这点要注意区分,到底是组件没缓存(可以在setup里加个console.log('组件创建了'),跳转再返回看有没有打印),还是组件缓存了但数据没恢复。

Teleport组件会不会影响?

Teleport是Vue3用来把组件渲染到DOM树其他位置的API,比如弹窗、下拉菜单,如果被Keep-Alive缓存的组件里用到了Teleport,那会不会影响缓存?其实大部分情况下不会,Teleport只是把DOM移动了,组件实例还是属于原来的父组件,会跟着父组件一起被缓存,但有一种特殊情况:Teleport的目标元素(to属性)在组件外部,而且目标元素在组件失活时被销毁了——比如把弹窗渲染到document.body上没问题,但如果渲染到某个v-if控制的div上,那个div在组件失活后被销毁了,再激活组件时,Teleport就找不到目标元素了,可能会导致组件渲染异常,但不会直接让Keep-Alive不生效,这点要分清楚。

Suspense组件和Keep-Alive一起用的话,要注意什么?

Suspense是Vue3用来处理异步组件加载的API,比如用defineAsyncComponent加载一个需要网络请求的组件,或者组件内部有<Suspense><template #default>...</template><template #fallback>加载中...</template></Suspense>,这时候如果把Suspense放在Keep-Alive外面,或者反过来,都可能出问题,正确的做法是:如果要缓存异步组件,Keep-Alive要包裹Suspense的直接子元素(也就是异步组件或者default插槽的内容),而不是Suspense本身,比如可以写成<keep-alive include="AsyncList"><Suspense><template #default><AsyncList /></template><template #fallback>加载中...</template></Suspense></keep-alive>,这样异步组件加载完成后,实例会被缓存,下次再用的时候就直接显示了,不会重新加载。

还有,如果异步组件用了defineAsyncComponentsuspensible属性(默认是true),那必须用Suspense包裹,不然组件会直接报错,更别说缓存了。

还有一些比较隐蔽的踩坑场景,你遇到过吗?

除了上面说的常见情况和Vue3特有的坑,还有一些比较隐蔽的,比如路由配置的问题、组件复用的问题、状态管理的问题,我也整理了两个最常见的:

路由用了replace或者query/hash没变?

不对,路由用replace是不会影响缓存的,只是不会在历史记录里留痕迹,那query/hash没变的话呢?比如从/list?page=1跳到/list?page=2,再跳回/list?page=1——这时候因为路由的path和name都没变,Vue Router默认会复用组件实例,也就是不会触发created、mounted,但是会触发beforeRouteUpdate、onUpdated(Vue2是beforeRouteUpdate、updated),这时候很多人会觉得“Keep-Alive没生效,因为页面重新渲染了”,但其实是组件实例被复用了,不是重新创建,这时候只需要在beforeRouteUpdate或者onUpdated里更新数据就行了,不需要改Keep-Alive的配置,如果不想复用组件实例,只想让Keep-Alive缓存不同query/hash的同一组件,那可以在router-view上加个key,比如<router-view :key="$route.fullPath" />,这样每次query/hash变了,key就变了,组件实例就会重新创建,但同时Keep-Alive也会缓存不同key对应的实例——不过这时候要注意max属性,别缓存太多实例导致内存占用过高。

状态管理里的数据被重置了?

很多人用Vuex或者Pinia来管理全局状态,比如列表页的筛选条件和页码存在Pinia里,然后跳转详情再返回,发现数据没了,就以为是Keep-Alive没生效,这时候先别急着改Keep-Alive,先看看状态管理里的代码:是不是在组件的onUnmounted里把数据重置了?是不是在路由守卫里把数据重置了?如果是的话,就算组件被Keep-Alive缓存了,再激活时数据也会被重置,正确的做法是:如果数据只需要在当前会话(比如打开主内容区到关闭主内容区)保留,那可以在主内容区的根组件的onUnmounted里重置;如果数据需要永久保留(比如刷新页面后还在),那可以用localStorage或者sessionStorage来存储。

教你一个快速排查的方法

上面说了这么多踩坑场景,可能一时半会儿记不住,教你一个快速排查的三步法:

  1. 先看组件有没有被创建两次:在组件的setup里加个console.log('组件创建', Date.now()),然后跳转再返回,看有没有打印两次——如果打印了两次,说明组件没被缓存,去检查基础条件(包裹方式、include/exclude/max、v-if);
  2. 如果组件只打印了一次,说明被缓存了,再看数据有没有恢复:检查onActivated有没有被触发(加个console.log('组件激活')),如果没触发,检查setup里有没有导入onActivated;如果触发了,检查数据恢复的代码有没有写对;
  3. 如果上面都没问题,再看有没有Vue3特有的坑或者隐蔽的坑:比如用了Teleport、Suspense,或者状态管理里的数据被重置了。

其实Vue3的Keep-Alive和Vue2相比,核心逻辑没怎么变,只是新增了一些和新语法、新API配合的注意事项,只要按照这个快速排查法一步一步来,基本都能找到问题所在。

版权声明

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

热门