Vue3中怎么精准watch监听路由变化?看完就能上手各种场景
为什么很多人在Vue3里监听路由会踩坑?
刚从Vue2转过来的开发者,大概率第一反应还是直接用watch监听this.$route或者useRoute()返回的对象,但一上手就发现不对:要么点击同一个页面参数不变时没反应,要么切换页面带不带query都触发太频繁,要么props接收路由参数时还得再写一套watch逻辑,完全不是预期的效果,其实核心原因是Vue3和Vue2的路由监听底层机制、响应式写法都有变化,不能直接照搬旧经验。
Vue3提供了哪几种监听路由的主流方式?
目前主流的、官方推荐或者社区高频使用的有4种,分别适合不同的场景:直接watch useRoute()的全量对象、监听useRoute()的特定属性、使用onBeforeRouteUpdate导航守卫、开启路由组件的props传参模式后直接watch props,每种方式的性能开销、触发条件、适用范围都不一样,下面会逐个拆解带案例。
第一种:直接watch全量useRoute对象,什么时候用?
直接监听route对象本身是最基础的写法,但一定要加deep: true,因为useRoute()返回的是一个响应式的Proxy对象,直接浅监听只会检测路由对象的引用变化——而Vue Router 4为了性能优化,大多数情况下切换路由只会修改内部属性,不会重新生成新对象,浅监听是不会生效的。
那什么时候用这种呢?其实很少有场景必须用到全量监听,因为它的性能最差:只要路由的path、query、params、meta、hash任何一个属性变了,都会触发回调,甚至有时候内部的响应式属性被意外修改(比如手动改了params的某个值,虽然官方不建议这么做)也会触发,如果非要用,通常是在一些全局导航相关的组件里,比如侧边栏需要根据当前路由的所有属性做高亮或者权限判断,但这个场景更推荐用computed结合meta或者path,而非watch,因为computed是懒执行的,性能更好。
这里给一个全量监听的小案例,要注意写法:
<script setup>
import { useRoute, watch } from 'vue'
const route = useRoute()
// 一定要加deep: true
watch(route, (newRoute, oldRoute) => {
console.log('路由全量属性变化了!')
console.log('新路径:', newRoute.path)
console.log('旧路径:', oldRoute.path)
}, { deep: true })
</script>
第二种:监听useRoute的特定属性,精准度最高!
这是我最推荐的日常开发写法,因为可以精准控制只监听需要变化的属性,比如只监听path(切换页面层级)、只监听params(同页面不同ID的详情页)、只监听query(同页面筛选条件变化),不仅性能好,逻辑也清晰,不会误触发。
比如做一个电商的商品详情页,需要根据商品ID(params里的id)去重新请求接口,这时候只需要监听route.params.id就可以了,不用管query或者其他属性;如果做商品列表页,需要根据筛选条件(query里的sort、page、category)重新请求,就监听这些属性的组合或者单独监听。
这里要提一个关键点:Vue3的watch可以监听多个响应式源,组成一个数组,只要其中一个变化就触发,还能配合immediate: true,组件挂载后立刻执行一次回调,不用再单独写mounted里的请求逻辑,非常方便。
给两个常用的精准监听案例:
- 只监听商品详情页的params.id
<script setup> import { useRoute, watch } from 'vue' import { getProductDetail } from '@/api/product' const route = useRoute()
const fetchDetail = async (id) => { const res = await getProductDetail(id) // 处理数据 }
// immediate: true 组件挂载后立刻请求 // 直接监听解构后的params.id?其实可以,但要注意解构后如果不是响应式的?不,route本身是响应式的,解构出来的单个属性如果是基础类型(比如id是字符串或数字),要转成getter函数,或者用toRefs // 推荐用toRefs的写法,更规范,避免响应式丢失 import { toRefs } from 'vue' const { params } = toRefs(route) watch(() => params.value.id, (newId) => { fetchDetail(newId) }, { immediate: true })
``` 2. 监听商品列表页的多个query参数 ```vue ```第三种:使用onBeforeRouteUpdate导航守卫,适合带组件缓存的场景
如果你的项目用了<keep-alive>缓存路由组件,这时候前两种watch写法会不会有问题?比如从商品列表页跳到商品A详情页,再跳到商品B详情页,这时候详情页组件被缓存了,mounted只会执行一次,但前两种watch写法依然会生效,因为route的属性还是在变化的——不过onBeforeRouteUpdate导航守卫有它的独特优势:它是在路由更新、组件复用之前触发的,可以拿到oldRoute和newRoute,还能阻止路由更新(虽然很少用到),而且更符合Vue Router的导航流程。
onBeforeRouteUpdate只能在路由组件(也就是在router/index.js里直接配置的组件,不是子组件)里使用,子组件要用的话,可以通过props把导航守卫里的参数传下去,或者用provide/inject,或者直接用前两种watch写法。
给一个带keep-alive的商品详情页用onBeforeRouteUpdate的案例:
<script setup>
import { onBeforeRouteUpdate } from 'vue-router'
import { getProductDetail } from '@/api/product'
const fetchDetail = async (id) => {
const res = await getProductDetail(id)
// 处理数据
}
// 组件挂载后的第一次请求,还是需要单独写mounted或者用watch的immediate
// 这里为了对比,用mounted
import { onMounted } from 'vue'
import { useRoute } from 'vue'
const route = useRoute()
onMounted(() => {
fetchDetail(route.params.id)
})
// 复用组件时的请求,用onBeforeRouteUpdate
onBeforeRouteUpdate(async (to, from) => {
console.log('从', from.path, '跳转到', to.path)
// 这里可以判断是否是同一个页面的参数变化,比如to.path === from.path
if (to.path === from.path && to.params.id !== from.params.id) {
await fetchDetail(to.params.id)
// 如果有loading或者滚动到顶部的操作,也可以在这里做
}
})
</script>
这里补充一句:有时候用keep-alive时,我们可能还需要onActivated和onDeactivated钩子,但它们和监听路由变化的关系不大,主要是处理组件激活和失活时的状态,比如暂停视频播放、重置滚动位置等。
第四种:开启路由组件的props传参模式后直接watch props
这是官方非常推荐的一种解耦写法,因为它可以让路由组件不依赖于useRoute,也就是说,你可以把这个组件当成普通组件来用,通过props传入参数,测试的时候也不用模拟路由环境,非常方便。
开启props传参模式的方法很简单,在router/index.js里配置路由的时候,把对应的组件的props属性设为true(只传params)、false(不传,默认)、或者一个函数(自定义传params、query、meta等)。
给两种常用的props传参模式案例:
- 只传params,设为true
// router/index.js import { createRouter, createWebHistory } from 'vue-router' import ProductDetail from '@/views/ProductDetail.vue'
const routes = [ { path: '/product/:id', name: 'ProductDetail', component: ProductDetail, props: true // 把params里的所有属性作为props传给组件 } ]
const router = createRouter({ history: createWebHistory(), routes })
export default router
```vue
<!-- ProductDetail.vue -->
<script setup>
// 这里不需要useRoute了,直接用props接收id
const props = defineProps(['id'])
import { watch } from 'vue'
import { getProductDetail } from '@/api/product'
const fetchDetail = async (id) => {
const res = await getProductDetail(id)
// 处理数据
}
watch(() => props.id, (newId) => {
fetchDetail(newId)
}, { immediate: true })
</script>
- 自定义传query,设为函数
// router/index.js const routes = [ { path: '/product-list', name: 'ProductList', component: () => import('@/views/ProductList.vue'), // 自定义传参函数,返回的对象作为props props: (route) => ({ sort: route.query.sort || 'default', page: route.query.page || 1, category: route.query.category || 'all' }) } ]<!-- ProductList.vue --> <script setup> const props = defineProps(['sort', 'page', 'category']) import { watch } from 'vue' import { getProductList } from '@/api/product'
const fetchList = async (sort, page, category) => { const res = await getProductList({ sort, page, category }) // 处理数据 }
watch( [() => props.sort, () => props.page, () => props.category], ([newSort, newPage, newCategory]) => { fetchList(newSort, newPage, newCategory) }, { immediate: true } )
```不同场景该选哪种方式?
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 普通子组件需要响应路由变化 | 第二种(监听useRoute的特定属性) | 子组件不能用onBeforeRouteUpdate,解耦需求不高的话直接用最方便 |
| 路由组件有解耦/测试需求 | 第四种(开启props传参后watch props) | 不依赖useRoute,可复用性、可测试性强 |
| 路由组件用了keep-alive,需要在复用前做特殊处理 | 第三种(onBeforeRouteUpdate) | 在导航流程前触发,能拿到新旧路由,还能阻止更新 |
| 极少数需要监听所有路由属性的全局场景 | 第一种(全量watch useRoute加deep: true) | 性能最差,尽量用computed代替 |
最后再提醒几个常见的踩坑点:
- 如果要在watch里用async/await,没问题,但要注意如果多次触发请求,可能会有竞态问题——比如先请求商品A,再请求商品B,但商品A的接口返回慢,后覆盖商品B的数据,这时候可以用
AbortController来取消上一次的请求。 - 手动修改
useRoute()返回的对象的属性(比如params、query)是无效的,官方不建议这么做,要修改路由的话,要用useRouter()返回的push或replace方法。 - 如果监听的是解构后的单个基础类型属性,一定要转成getter函数或者用
toRefs/toRef包裹,否则会丢失响应式。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

