Vue3项目里路由组件切回来数据没刷新?试试activated钩子,但这5个坑你别踩!
很多刚从Vue2转过来做keep-alive页面缓存的开发者,或者第一次接触路由切换优化的朋友,肯定遇到过这个场景:列表页跳转详情页,修改了点内容点返回,或者筛选了商品跳出去看别的页面再回来,原来的查询结果、滚动条位置要么全没了要么不对,手动去onMounted里加刷新又会每次进页面都重刷,完全没发挥缓存的作用,这时候,activated钩子就派上用场了——但很多人用不好它,要么数据重复请求,要么失效,要么搞混和onMounted、deactivated的关系,今天咱们就把这个钩子说透,从基础原理到避坑指南,再到实际应用场景,给你理得明明白白。
activated和deactivated到底是什么?它们和keep-alive是绑定关系吗?
先搞清楚最核心的:这俩钩子必须配合keep-alive组件使用才会生效,脱离了keep-alive,它们就像没通电的开关,按烂了也不会触发。
那keep-alive是什么?简单说就是Vue给你封装的一个“缓存抽屉”,你把需要保留状态的组件塞进去,它被其他组件覆盖(比如路由跳转、v-if/v-else切换展示的keep-alive包裹组件)的时候,不会真的被销毁,而是被塞进抽屉里“休眠”;等再把它拉出来展示的时候,也不会重新创建实例,直接“唤醒”就行——这时候就会触发activated钩子,而塞进去的时候触发deactivated。
对比一下普通组件的生命周期钩子:
- 普通组件(没套keep-alive):onBeforeMount → onMounted → onBeforeUnmount → onUnmounted
- keep-alive包裹组件:首次加载是 onBeforeMount → onMounted → activated;之后每次“唤醒”是 activated,每次“休眠”是 deactivated;最后彻底不需要的时候(比如keep-alive的include/exclude移除了这个组件,或者外层keep-alive容器被销毁),才会触发 onBeforeUnmount → onUnmounted。
举个最直观的例子:你有一个“我的购物车”页面,套了keep-alive缓存已选商品的数量、价格和滚动位置,第一次点“我的购物车”会走完首次加载的钩子;然后你去逛“商品推荐”,这时候“我的购物车”进入deactivated,休眠了;再点回“购物车”,activated触发,这里你可以加一些实时性强的请求,比如检查商品库存有没有变,优惠券有没有过期,同时保留原来的滚动和已选状态;如果用户退出登录,你把购物车从keep-alive的include里删掉,再重新登录,购物车就会彻底销毁重建,走首次加载的钩子。
什么时候该用activated,什么时候该用onMounted?别搞反了
很多新手踩的第一个坑,就是把所有刷新逻辑都塞到onMounted里,缓存了页面还是没用,或者全塞到activated里,首次加载和每次唤醒都重复请求,浪费带宽和服务器资源,那怎么区分场景呢?
优先用onMounted的场景:
- 只需要在组件第一次创建并挂载时执行的一次性初始化:比如注册全局监听事件但只注册一次(不过记得在deactivated里暂时取消监听,彻底销毁时再彻底取消,不然重复唤醒会重复触发)、加载只需要存在一次的静态资源配置(比如地图SDK的临时密钥校验,缓存页面不需要每次都校验)、获取页面所需的基础静态字典数据(比如性别、城市列表,除非有特殊刷新需求)。
- 页面状态对离开期间的外部变化不敏感,或者外部变化完全不会影响这个组件的:比如纯展示的个人信息页(除非你自己做了修改通知机制,不然可以不用每次激活都刷)。
优先用activated的场景:
- 页面状态对离开期间的外部实时性变化敏感:比如前面说的购物车库存、优惠券过期时间、股票走势页、实时在线人数统计页。
- 页面的筛选、搜索、分页状态需要保留,但数据本身可能已经更新:比如电商商品列表页,你跳转到详情页看了下A商品的库存充足,然后去后台(假设和前端是同步测试环境)把A商品库存设为0,再返回列表页,要是只保留了筛选状态不刷新数据,用户就会看到A商品还在有货列表里,点进去才发现不能买,体验极差——这时候就可以在activated里只刷新筛选好的当前页数据,保留滚动、筛选、页码这些用户刚才的操作状态。
- 需要恢复离开页面时暂停的功能:比如直播列表页的自动轮播、视频播放页的暂停/播放状态(当然这里可能还要配合deactivated里的暂停逻辑)、定时器任务(比如倒计时优惠券,离开页面时暂停,回来继续走)。
那有没有两者都要用的场景?
当然有!比如刚才的直播列表页:onMounted里可以获取直播列表的静态框架、注册轮播的点击事件、获取初始的直播场次;然后activated里恢复轮播的自动播放、获取最新的直播在线人数和状态(比如有没有新开播、有没有停播);deactivated里暂停轮播、取消直播状态的实时监听(比如WebSocket连接或者短轮询定时器)。
activated里发请求会不会导致数据重复?3个实用的解决方法
这是第二个超级高频的坑:每次页面激活都发一模一样的请求,比如首次加载onMounted刷了一次商品列表,跳详情页再回来activated又刷一次,虽然用户感觉不到卡顿,但服务器会收到两次请求,带宽和资源都浪费了,要是请求接口有幂等性问题还可能出bug,怎么避免呢?
判断请求是否已经完成,或者设置“是否首次激活”的标志位
比如我们可以用ref定义一个isFirstActivated,初始值设为true,在第一次activated执行完请求之后把它改成false,之后的激活就不再刷数据了——但这个方法只适合那种离开期间不需要更新数据的场景,比如纯静态筛选状态保留的列表页,外部数据不会变的那种。
如果需要部分更新或者条件更新,比如刷新间隔超过5分钟才刷最新数据,那可以用ref定义一个lastRequestTime,每次激活的时候先算一下当前时间和上次请求时间的差值,超过阈值才发请求。
举个条件更新的小代码片段(伪代码,重点看逻辑):
import { ref, onMounted, activated } from 'vue'
import { getGoodsList } from '@/api/goods'
const goodsList = ref([])
const lastRequestTime = ref(0)
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5分钟刷新一次
// 获取商品列表的公共函数
const fetchGoodsList = async () => {
try {
const res = await getGoodsList(/* 这里传你的筛选、分页参数 */)
goodsList.value = res.data.list
lastRequestTime.value = Date.now()
} catch (err) {
console.error('获取商品列表失败:', err)
}
}
// 首次加载必须刷
onMounted(() => {
fetchGoodsList()
})
// 激活时判断时间差
activated(() => {
const now = Date.now()
if (now - lastRequestTime.value > REFRESH_INTERVAL) {
fetchGoodsList()
}
})
利用Vue Router的导航守卫,或者组件外的状态管理(Pinia/Vuex)判断是否需要刷新
如果刷新逻辑和路由跳转的来源或者参数变化有关,比如从详情页修改了商品状态返回需要刷新,从商品推荐页返回不需要刷新,那可以用Pinia/Vuex存一个needRefreshGoodsList的标志位,在详情页修改完商品状态后把标志位设为true,然后在商品列表页的activated里先检查这个标志位,是true就刷新,刷新完再设为false。
如果不想用状态管理,也可以用Vue Router的useRoute的meta属性,或者beforeRouteEnter/beforeRouteUpdate导航守卫:比如在详情页跳转回来的时候,给路由的meta里加个refresh: true,然后在商品列表页的activated里判断route.meta.refresh,是true就刷,刷完把meta.refresh改成false,不过这里要注意,meta是共享的,可能会被其他页面误改,所以状态管理会更稳妥一点。
利用keep-alive的max属性或者动态的include/exclude属性,控制组件的缓存时间
比如你可以给keep-alive设置max="10",最多缓存10个组件,超过10个的话,最早缓存的那个会被销毁,下次进来就会重新走onMounted,自动刷新数据——适合那种缓存不需要太久,组件数量也不会太多的场景。
或者你可以在用户执行某些操作后,比如退出登录、清空购物车、修改收货地址,把对应的组件从include里删掉,下次进来就会重新创建,自动刷新。
activated和deactivated里的事件监听、定时器、WebSocket连接怎么处理?不然会内存泄漏或者功能异常!
这是第三个超级严重的坑!很多人在onMounted里注册了事件监听、开了定时器或者建立了WebSocket连接,然后只在onBeforeUnmount里清理,但keep-alive包裹的组件如果不被彻底销毁,onBeforeUnmount是不会触发的——这就会导致内存泄漏(比如事件监听器重复注册,每次激活都加一个,下次触发事件的时候会执行多次)、功能异常(比如定时器一直跑,离开直播列表页了还在轮播数据,浪费流量)。
那正确的清理逻辑应该是什么样的?分两步走:临时清理和彻底清理。
临时清理:放在deactivated里
当组件被塞进缓存抽屉休眠的时候,我们不需要彻底删除这些东西,但需要暂停它们的工作,避免重复消耗资源和重复触发:
- 事件监听:比如全局的
resize、scroll监听,还有WebSocket的消息监听,可以用removeEventListener或者off方法暂时取消,下次激活的时候再重新注册。 - 定时器:比如短轮询、倒计时,可以用
clearInterval或者clearTimeout暂停,下次激活的时候再用保存的参数重新开启。 - WebSocket连接:如果只是离开一小会儿,可以先不关闭连接,只取消消息监听;如果离开时间可能比较长,可以暂时关闭连接,下次激活的时候再重新建立——不过关闭再重建可能会有一点延迟,这个要根据你的业务场景来选。
彻底清理:放在onBeforeUnmount里
当组件被彻底从缓存抽屉里拿出来销毁的时候,我们需要彻底删除这些东西,释放内存:
- 事件监听:确保已经取消所有注册的监听,不管是全局的还是组件内的。
- 定时器:确保已经清除所有的定时器。
- WebSocket连接:确保已经关闭连接,并且清理了所有的回调函数。
举个WebSocket实时在线人数统计的小代码片段(伪代码):
import { ref, onMounted, onBeforeUnmount, activated, deactivated } from 'vue'
const onlineCount = ref(0)
let ws = null
let reconnectTimer = null // 重连定时器
const RECONNECT_INTERVAL = 3 * 1000 // 重连间隔3秒
// 建立WebSocket连接的公共函数
const connectWS = () => {
ws = new WebSocket('wss://your-websocket-url.com/online')
ws.onopen = () => {
console.log('WebSocket连接成功')
if (reconnectTimer) {
clearInterval(reconnectTimer)
reconnectTimer = null
}
}
ws.onmessage = (e) => {
const data = JSON.parse(e.data)
onlineCount.value = data.onlineCount
}
ws.onerror = (err) => {
console.error('WebSocket连接失败:', err)
// 连接失败自动重连,这里的重连逻辑要注意,如果是主动关闭的就不要重连
if (!ws.isClosedByUser) {
startReconnect()
}
}
ws.onclose = (e) => {
console.log('WebSocket连接关闭:', e)
// 连接关闭自动重连,主动关闭的除外
if (!ws.isClosedByUser) {
startReconnect()
}
}
}
// 开始重连的公共函数
const startReconnect = () => {
if (reconnectTimer) return
reconnectTimer = setInterval(() => {
connectWS()
}, RECONNECT_INTERVAL)
}
// 临时取消监听的公共函数
const pauseWS = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
// 先取消消息监听,避免接收不到消息
ws.onmessage = null
// 这里先不关闭连接,只取消监听,下次激活的时候再恢复
// 当然你也可以选择关闭连接,看业务场景
}
}
// 恢复监听的公共函数
const resumeWS = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
// 恢复消息监听
ws.onmessage = (e) => {
const data = JSON.parse(e.data)
onlineCount.value = data.onlineCount
}
} else {
// 如果连接已经断开,重新建立
connectWS()
}
}
// 彻底关闭连接的公共函数
const closeWS = () => {
if (ws) {
ws.isClosedByUser = true
ws.close()
ws = null
}
if (reconnectTimer) {
clearInterval(reconnectTimer)
reconnectTimer = null
}
}
// 首次加载
onMounted(() => {
connectWS()
})
// 激活时恢复
activated(() => {
resumeWS()
})
// 休眠时临时取消
deactivated(() => {
pauseWS()
})
// 彻底销毁时清理
onBeforeUnmount(() => {
closeWS()
})
activated在Composition API和Options API里的写法一样吗?不一样,别搞混了
第四个坑:刚从Options API转Composition API的朋友,容易直接在setup里写activated(),但这是不对的——Composition API里的activated和其他生命周期钩子一样,需要从vue包里显式导入,然后在setup函数里调用。
对比一下两种写法:
Options API写法:
export default {
name: 'GoodsList',
data() {
return {
goodsList: []
}
},
methods: {
async fetchGoodsList() {
// 这里发请求
}
},
mounted() {
this.fetchGoodsList()
},
activated() {
// 这里判断时间差或者标志位,决定是否刷新
this.fetchGoodsList()
},
deactivated() {
// 这里临时清理
},
beforeUnmount() {
// 这里彻底清理
}
}
Composition API写法(重点,现在大部分Vue3项目都是用这个):
import { ref, onMounted, onBeforeUnmount, activated, deactivated } from 'vue'
import { getGoodsList } from '@/api/goods'
export default {
name: 'GoodsList',
setup() {
const goodsList = ref([])
const lastRequestTime = ref(0)
const REFRESH_INTERVAL = 5 * 60 * 1000
const fetchGoodsList = async () => {
try {
const res = await getGoodsList(/* 参数 */)
goodsList.value = res.data.list
lastRequestTime.value = Date.now()
} catch (err) {
console.error(err)
}
}
onMounted(() => {
fetchGoodsList()
})
activated(() => {
const now = Date.now()
if (now - lastRequestTime.value > REFRESH_INTERVAL) {
fetchGoodsList()
}
})
deactivated(() => {
// 临时清理
})
onBeforeUnmount(() => {
// 彻底清理
})
return {
goodsList
}
}
}
还要注意一个点:在Composition API的<script setup>语法糖里,写法和上面的setup函数里是一样的,直接导入钩子然后调用就行,不需要return什么(除非要暴露给模板用的变量和函数,但<script setup>里默认暴露给模板)。
activated里能不能获取到DOM元素?可以,但要注意时机吗?
第五个坑:很多人担心组件被唤醒的时候DOM还没更新,获取不到DOM元素——其实不用太担心,activated钩子触发的时候,组件的DOM已经被重新插入到文档流中,并且已经渲染完成了,所以可以直接用ref获取DOM元素,不需要用nextTick。
不过这里有个例外:如果在activated里修改了某些响应式数据,导致DOM重新渲染,那修改之后再获取新的DOM元素,还是需要用nextTick的——这个和onMounted里的情况是一样的,不管哪个生命周期钩子,只要修改了响应式数据后要获取更新后的DOM,都得用nextTick。
举个获取DOM滚动位置并恢复的小例子(结合activated和deactivated):
import { ref, onMounted, activated, deactivated, nextTick } from 'vue'
export default {
name: 'GoodsList',
setup() {
const listContainerRef = ref(null)
const savedScrollTop = ref(0)
// 首次加载可以不用管滚动位置,或者从localStorage里恢复上次退出应用的位置
onMounted(() => {
// 从localStorage恢复
const saved = localStorage.getItem('goodsListScrollTop')
if (saved && listContainerRef.value) {
listContainerRef.value.scrollTop = parseInt(saved)
}
})
// 唤醒时恢复滚动位置
activated(() => {
if (listContainerRef.value) {
listContainerRef.value.scrollTop = savedScrollTop.value
}
})
// 休眠时保存滚动位置
deactivated(() => {
if (listContainerRef.value) {
savedScrollTop.value = listContainerRef.value.scrollTop
// 也可以顺便存到localStorage里,防止刷新应用丢失
localStorage.setItem('goodsListScrollTop', savedScrollTop.value)
}
})
// 这里模拟修改数据后获取新DOM的情况
const addNewGoods = () => {
// 假设这里添加了一个新商品到列表里
// 添加之后要滚动到列表底部,需要用nextTick
nextTick(() => {
if (listContainerRef.value) {
listContainerRef.value.scrollTop = listContainerRef.value.scrollHeight
}
})
}
return {
listContainerRef,
addNewGoods
}
}
}
用好activated钩子的5个小技巧
今天讲了这么多,最后给大家总结一下,用好activated钩子的5个小技巧,避免踩坑:
- 必须配合keep-alive使用,脱离了keep-alive它就是个空钩子。
- 分清onMounted和activated的使用场景,不要搞反了,避免重复请求或者状态丢失。
- 请求数据前加判断条件,比如首次激活标志位、时间差、状态管理里的刷新标志位,避免重复请求。
- 事件监听、定时器、WebSocket分两步清理:临时清理放在deactivated里,彻底清理放在onBeforeUnmount里,避免内存泄漏和功能异常。
- Composition API里要显式导入activated钩子,不要直接在setup里写函数名。
如果还有什么关于Vue3 activated钩子的问题,或者其他Vue3开发的问题,欢迎在评论区留言讨论!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

