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

Vue3中怎么精准watch监听路由变化?看完就能上手各种场景

terry 2天前 阅读数 288 #Vue
文章标签 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里的请求逻辑,非常方便。

给两个常用的精准监听案例:

  1. 只监听商品详情页的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时,我们可能还需要onActivatedonDeactivated钩子,但它们和监听路由变化的关系不大,主要是处理组件激活和失活时的状态,比如暂停视频播放、重置滚动位置等。

第四种:开启路由组件的props传参模式后直接watch props

这是官方非常推荐的一种解耦写法,因为它可以让路由组件不依赖于useRoute,也就是说,你可以把这个组件当成普通组件来用,通过props传入参数,测试的时候也不用模拟路由环境,非常方便。

开启props传参模式的方法很简单,在router/index.js里配置路由的时候,把对应的组件的props属性设为true(只传params)、false(不传,默认)、或者一个函数(自定义传params、query、meta等)。

给两种常用的props传参模式案例:

  1. 只传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>
  1. 自定义传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代替

最后再提醒几个常见的踩坑点:

  1. 如果要在watch里用async/await,没问题,但要注意如果多次触发请求,可能会有竞态问题——比如先请求商品A,再请求商品B,但商品A的接口返回慢,后覆盖商品B的数据,这时候可以用AbortController来取消上一次的请求。
  2. 手动修改useRoute()返回的对象的属性(比如params、query)是无效的,官方不建议这么做,要修改路由的话,要用useRouter()返回的pushreplace方法。
  3. 如果监听的是解构后的单个基础类型属性,一定要转成getter函数或者用toRefs/toRef包裹,否则会丢失响应式。

版权声明

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

热门