或者yarn add lodash-es
Vue3 怎么监听路由query变化 踩坑全解决附实战代码
在开发带搜索、筛选、分页的页面时,路由query参数简直是个宝——既能把当前状态存进地址栏分享给别人,刷新后还能自动还原,体验感拉满,但真上手Vue3 watch route query,很多人要么发现没反应,要么刷新了状态对不上,要么频繁触发接口请求卡成狗,别急,今天咱们把所有坑挖出来填实,从基础写法到进阶优化再到完整实战,一步步讲明白。
为什么要选watch监听route.query,而不用其他方法?
很多刚接触Vue3路由的同学,会先想到用onMounted+onUpdated这种生命周期组合拳,或者setup里直接访问route.query当响应式数据,但这两种方法要么太麻烦要么有局限,watch才是最优解,咱们对比着说清楚。
生命周期组合拳为什么不行?
生命周期的逻辑大概是这样的:onMounted的时候,先拿一次初始的route.query,去请求数据或者初始化组件;onUpdated的时候,再检查route.query有没有变,变了再执行后续操作,但这里有两个大问题: 第一个问题是“组件会不会更新”的不确定性,如果你的页面里没有其他响应式数据变化,只有route.query变了,而你绑定route.query的地方很少甚至没有,那onUpdated根本不会触发!举个例子,你做了个只有后端返回列表的纯展示页,整个组件的template只用到了后端返回的list,没有直接用route.query,那query从?page=1跳到?page=2,模板不会变,onUpdated就不会执行,你自然拿不到新的query,页面也不会更新。 第二个问题是逻辑太分散,难维护,初始逻辑放在onMounted,变化逻辑放在onUpdated,如果逻辑复杂(比如参数校验、防抖节流、缓存处理),这两个钩子得写重复的代码,改起来也容易漏,还有更细的坑,比如路由切换前组件销毁,onUpdated可能不会触发最后一次;或者路由是keep-alive缓存的,组件不会重新mount,但query变了,onMounted不跑,这时候状态全乱。
setup里直接访问route.query为什么有隐患?
如果你用的是Vue Router 4的Composition API(也就是import { useRoute } from 'vue-router'那种),那useRoute返回的route对象本身是响应式的,对吧?但这里的响应式是“浅层响应式”!Vue Router的官方文档里明确提过这点,但很多人没注意。
浅层响应式意味着什么?只有route.path、route.query这种顶层属性的引用变化才会触发响应,可query是个对象啊!如果你的页面是从?keyword=vue跳到?keyword=react,那整个route.query的引用其实没变,只是它里面的keyword属性变了,这时候你直接在template里用route.query.keyword没问题,Vue会自动追踪到深层属性,但如果你在setup里用computed或者直接写逻辑(比如直接if(route.query.keyword)请求数据),那除非你加了deep或者访问的时候显式触发深层属性(比如解构的时候用toRefs或者单个toRef),否则逻辑不会自动执行!
举个反例,很多新手会这么写:
// setup里
import { useRoute, onMounted } from 'vue-router'
import { getList } from '@/api/list'
const route = useRoute()
onMounted(() => {
getList({ page: route.query.page || 1, keyword: route.query.keyword || '' })
})
// 想当然以为query变了会自动跑,结果根本不会
这种写法刷新是没问题,但切换query参数页面死都不动,就是浅层响应式惹的祸。
所以综合来看,watch route.query才是既稳又灵活的方案——既能监听query的所有变化(不管是顶层引用还是深层属性),又能把初始逻辑和变化逻辑合并在一起,还能配合各种配置项解决各种问题。
watch route.query的3种基础写法,你用对了吗?
既然选了watch,那接下来就是写对,这里要分三种情况,分别对应不同的需求场景,大家别乱用,不然还是会踩坑。
情况1:query有增删/整个对象替换时才监听——用普通watch+直接监听route.query
这种情况用得比较少,但如果你的项目里有这种逻辑(比如从没有keyword参数的状态,突然跳到有keyword的状态,才触发某些全局弹窗提示),那可以用这种写法:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 直接监听route.query,不用加任何配置
watch(route.query, (newQuery, oldQuery) => {
console.log('整个query对象变了!', newQuery, oldQuery)
// 你的逻辑
})
为什么这种情况用得少?因为刚才说过,Vue Router 4的route.query是保持引用不变的!除非你用了类似router.replace({ query: {...} })或者router.push({ query: {...newObj} })这种显式创建新对象替换旧query的写法,否则普通的属性赋值(比如只改page或者keyword),整个route.query的引用不会变,普通watch根本不会触发!新手踩的第一个“watch没反应”的坑,大概率就是这种写法,但需求又只是改属性,所以白搭。
情况2:query的任意属性变化都要监听——用普通watch+{ deep: true }
这是最常用的场景了!比如搜索页、筛选页、分页页,不管改哪个参数,都要重新请求数据,这时候就加个deep:true配置:
import { watch } from 'vue'
import { useRoute } from 'vue-router'
import { getList } from '@/api/list'
const route = useRoute()
// 加deep:true,不管属性怎么变都监听
watch(
route.query,
(newQuery) => {
const params = {
page: parseInt(newQuery.page) || 1, // 注意query参数默认是字符串,要转成数字哦!
pageSize: parseInt(newQuery.pageSize) || 10,
keyword: newQuery.keyword || '',
status: newQuery.status || 'all'
}
getList(params)
},
{ deep: true }
)
哎,这里顺便提一句,query参数是字符串这个点也是个大踩坑点!很多人在逻辑里直接判断if(params.page === 1),结果page是字符串'1',条件不成立,接口请求参数错了,数据也出不来,找半天找不到原因,不管你是用push/replace传的数字还是字符串,路由解析出来的query永远是字符串类型,拿到手一定要先转类型!
不过这个写法虽然能解决“没反应”的问题,但又有个新坑:组件刚挂载的时候,watch不会自动执行!所以你刷新页面的时候,query明明有值,但接口不会请求,页面是空的,这时候怎么办?加个immediate:true配置!
情况3:组件刚挂载+属性任意变化都要监听——用普通watch+{ deep: true, immediate: true }
这就是最通用的“万金油”写法了,能覆盖90%的日常场景:
import { watch, ref } from 'vue'
import { useRoute } from 'vue-router'
import { getList } from '@/api/list'
const route = useRoute()
const list = ref([])
const loading = ref(false)
watch(
route.query,
async (newQuery) => {
loading.value = true
try {
const params = {
page: parseInt(newQuery.page) || 1,
pageSize: parseInt(newQuery.pageSize) || 10,
keyword: newQuery.keyword?.trim() || '', // 再加个trim防止用户输入空格
status: newQuery.status || 'all'
}
const res = await getList(params)
list.value = res.data.list
} catch (err) {
console.error('获取列表失败', err)
} finally {
loading.value = false
}
},
{ deep: true, immediate: true }
)
这样写之后,不管是刷新页面,还是改任意query参数,接口都会自动请求,loading也会正确显示,基本逻辑就通了。
万金油”写法虽然稳,但不是最优的,接下来咱们讲进阶优化,解决性能问题和更细的场景需求。
watch route.query的进阶优化,告别接口请求卡顿和无效监听
刚才的“万金油”写法有两个明显的问题:第一个是频繁改参数会频繁触发接口请求,比如用户快速输入搜索关键词,每敲一个字母就请求一次,既浪费服务器资源,又可能导致前一次请求的结果覆盖后一次(比如最后敲的是“react”,但前一次“reac”的请求慢,最后显示的是“reac”的结果,这就是“竞态问题”);第二个是加了deep:true之后,Vue会递归监听query对象的所有属性,虽然query对象一般不大,递归监听的性能损耗可以忽略,但如果能精准监听我们需要的属性,不是更优雅吗?
优化1:用防抖解决频繁请求的问题
防抖的原理很简单:当用户连续触发某个操作时,只有在操作停止后的指定时间内没有再触发,才会执行真正的逻辑,比如搜索关键词的防抖时间可以设为300-500毫秒,让用户敲完字停顿一下再请求。
不过这里要注意,Vue3的watch配置里没有debounce选项!很多新手会在watch的回调函数里直接写setTimeout,这其实是错的——因为每次触发watch都会创建一个新的setTimeout,不会清除上一个,结果还是会在指定时间后执行所有的请求,只是延迟了而已,正确的做法是用lodash-es的debounce函数(或者自己封装一个),然后把回调函数包裹起来,还要注意用箭头函数保持this指向(虽然setup里不用this,但习惯要养好),另外如果组件销毁了,要记得取消防抖函数,防止内存泄漏。
先安装lodash-es(因为要配合Vue3的Tree Shaking,不能直接用lodash的debounce,不然会把整个lodash包都打包进去,体积太大):
npm install lodash-es```
然后修改代码:
```javascript
import { watch, ref, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { getList } from '@/api/list'
import { debounce } from 'lodash-es'
const route = useRoute()
const list = ref([])
const loading = ref(false)
// 封装请求函数
const fetchList = async (newQuery) => {
loading.value = true
try {
const params = {
page: parseInt(newQuery.page) || 1,
pageSize: parseInt(newQuery.pageSize) || 10,
keyword: newQuery.keyword?.trim() || '',
status: newQuery.status || 'all'
}
const res = await getList(params)
list.value = res.data.list
} catch (err) {
console.error('获取列表失败', err)
} finally {
loading.value = false
}
}
// 用debounce包裹,防抖时间设为300毫秒,trailing设为true(默认就是true,意思是停止操作后执行),leading设为false(默认也是false,意思是操作开始时不执行)
// 这里注意!不要直接把debounce的返回值写在watch的回调里,要先赋值给一个变量,这样才能在onUnmounted里取消它
const debouncedFetchList = debounce(fetchList, 300)
// 组件销毁时取消防抖函数
onUnmounted(() => {
debouncedFetchList.cancel()
})
watch(
route.query,
(newQuery) => {
debouncedFetchList(newQuery)
},
{ deep: true, immediate: true }
)
这样修改之后,用户快速输入搜索关键词就不会频繁请求了,只有在停顿300毫秒后才会请求一次,体验感提升很多。
不过防抖解决了频繁请求的问题,但解决不了“竞态问题”——也就是前一次慢请求覆盖后一次快请求的问题,那怎么解决竞态问题呢?
优化2:用请求取消+请求ID解决竞态问题
请求取消的方法有很多种,如果你用的是axios的话,最简单的方法是用axios的CancelToken或者AbortController(推荐用AbortController,因为CancelToken已经被axios官方标记为弃用了)。
先修改你的axios封装文件,/utils/request.js:
import axios from 'axios'
import { ElMessage } from 'element-plus' // 假设用的是Element Plus
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 存放正在进行的请求的Map,key是请求的唯一标识,value是AbortController的实例
const pendingRequests = new Map()
// 生成请求的唯一标识的函数
const generateRequestKey = (config) => {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 添加请求到pendingRequests的函数
const addPendingRequest = (config) => {
// 如果已经有这个请求了,先取消上一个
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
pendingRequests.get(requestKey).abort()
pendingRequests.delete(requestKey)
}
// 创建新的AbortController实例,添加到pendingRequests
const controller = new AbortController()
config.signal = controller.signal
pendingRequests.set(requestKey, controller)
}
// 从pendingRequests中移除请求的函数
const removePendingRequest = (config) => {
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
pendingRequests.delete(requestKey)
}
}
// 请求拦截器
service.interceptors.request.use(
config => {
// 添加请求到pendingRequests
addPendingRequest(config)
// 这里可以添加token之类的逻辑
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// 从pendingRequests中移除请求
removePendingRequest(response.config)
const res = response.data
// 这里可以根据后端返回的状态码做判断
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
error => {
// 如果是请求被取消的,就不做处理
if (axios.isCancel(error) || error.name === 'AbortError') {
console.log('请求被取消')
} else {
// 从pendingRequests中移除请求
if (error.config) {
removePendingRequest(error.config)
}
ElMessage.error(error.message || '网络错误')
}
return Promise.reject(error)
}
)
export default service
这样修改之后,只要是重复的请求(method、url、params、data都一样),新的请求发起时就会自动取消上一个请求,完美解决竞态问题。
不过这里的请求唯一标识是根据method、url、params、data生成的,如果你的query参数变化很快,params也会变化很快,那每次的请求唯一标识都不一样,自动取消就不管用了,这时候可以给请求加一个自定义的ID,比如用时间戳或者一个全局的计数器,然后在请求拦截器里根据这个自定义ID来取消上一个请求。
修改一下@/api/list.js:
import request from '@/utils/request'
// 全局的计数器,每次请求加1
let requestId = 0
export const getList = (params) => {
return request({
url: '/api/list',
method: 'get',
params,
// 自定义的请求ID,用于取消上一个同类型的请求
customRequestId: 'getList'
})
}
然后修改@/utils/request.js里的generateRequestKey、addPendingRequest、removePendingRequest函数:
// 生成请求的唯一标识的函数
const generateRequestKey = (config) => {
// 如果有customRequestId,就用customRequestId作为唯一标识
if (config.customRequestId) {
return config.customRequestId
}
// 否则用原来的方法
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
这样不管params怎么变,只要customRequestId是'getList',新的请求发起时就会自动取消上一个同类型的请求,彻底解决竞态问题。
优化3:精准监听指定属性,减少不必要的递归监听
刚才说过,加了deep:true之后,Vue会递归监听query对象的所有属性,虽然性能损耗不大,但如果能精准监听我们需要的属性,不是更优雅吗?这时候可以用toRef或者toRefs来把我们需要的属性转成响应式引用,然后直接监听这些引用,不用加deep:true。
比如我们的页面只需要监听page、keyword、status这三个属性,pageSize是固定的,那就可以这么写:
import { watch, ref, toRef, toRefs, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { getList } from '@/api/list'
import { debounce } from 'lodash-es'
const route = useRoute()
// 方法1:用toRefs把整个route.query的属性转成响应式引用,然后解构出来
const { page, keyword, status } = toRefs(route.query)
// 方法2:如果只需要几个属性,用toRef更省内存
// const page = toRef(route.query, 'page')
// const keyword = toRef(route.query, 'keyword')
// const status = toRef(route.query, 'status')
const list = ref([])
const loading = ref(false)
const fetchList = async () => {
loading.value = true
try {
const params = {
page: parseInt(page.value) || 1,
pageSize: 10,
keyword: keyword.value?.trim() || '',
status: status.value || 'all'
}
const res = await getList(params)
list.value = res.data.list
} catch (err) {
console.error('获取列表失败', err)
} finally {
loading.value = false
}
}
const debouncedFetchList = debounce(fetchList, 300)
onUnmounted(() => {
debouncedFetchList.cancel()
})
// 直接监听这三个响应式引用组成的数组,不用加deep:true
watch(
[page, keyword, status],
() => {
debouncedFetchList()
},
{ immediate: true }
)
这样写之后,Vue只会监听这三个属性的变化,不会递归监听其他属性,更优雅也更高效,而且如果这三个属性中有一个是undefined,后来变成了有值,或者反过来,也会触发watch,比普通watch+deep:true更精准。
watch route.query的进阶场景:监听部分变化/路由回退时的特殊处理
刚才讲的都是通用场景,接下来讲两个比较常见的进阶场景,帮大家解决更细的需求。
进阶场景1:只监听部分属性变化,忽略其他属性变化
比如我们的页面有page、pageSize、keyword、status四个属性,其中pageSize是固定的下拉框,只有用户主动选择才会变化,变化的时候要重置page为1,然后再请求数据;而keyword和status变化的时候,也要重置page为1,然后再请求数据;但page变化的时候,不需要重置其他属性,直接请求数据就行,这时候怎么处理?
这里可以拆成两个watch:一个监听keyword、status、pageSize,变化的时候重置page为1,然后请求数据;另一个监听page,变化的时候直接请求数据,不过要注意,如果重置page为1的时候,会触发page的watch,导致请求两次数据,所以要加个判断或者用watchEffect?不对,watchEffect也会触发两次,更好的做法是用watch的第三个参数里的flush选项?或者在重置page的时候,用router.replace,并且设置replace的query里的page为1,然后让page的watch不要在这时候触发?
或者更简单的做法是,把page的变化和其他属性的变化分开处理,并且在处理其他属性变化的时候,先判断page是不是已经是1了,如果不是,就重置为1,并且不要在这时候触发page的watch?不对,好像不行,因为router.replace会修改route.query,route.query的page属性会变,不管用什么方法,page的watch都会触发,那怎么办?可以加一个全局的标志位,比如isResettingPage,当处理其他属性变化的时候,把isResettingPage设为true,然后重置page为1,这时候page的watch会判断isResettingPage,如果是true,就不请求数据,然后把isResettingPage设为false。
对,这个方法可行:
import { watch, ref, toRefs, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getList } from '@/api/list'
import { debounce } from 'lodash-es'
const route = useRoute()
const router = useRouter()
const { page, pageSize, keyword, status } = toRefs(route.query)
const list = ref([])
const loading = ref(false)
// 全局标志位,判断是否正在重置page
const isResettingPage = ref(false)
const fetchList = async () => {
loading.value = true
try {
const params = {
page: parseInt(page.value) || 1,
pageSize: parseInt(pageSize.value) || 10,
keyword: keyword.value?.trim() || '',
status: status.value || 'all'
}
const res = await getList(params)
list.value = res.data.list
} catch (err) {
console.error('获取列表失败', err)
} finally {
loading.value = false
}
}
const debouncedFetchList = debounce(fetchList, 300)
onUnmounted(() => {
debouncedFetchList.cancel()
})
// 第一个watch:监听page变化,直接请求数据,但如果是正在重置page,就不请求
watch(
page,
() => {
if (!isResettingPage.value) {
debouncedFetchList()
}
},
{ immediate: true }
)
// 第二个watch:监听pageSize、keyword、status变化,重置page为1,然后请求数据
watch(
[pageSize, keyword, status],
(newVals, oldVals) => {
// 注意oldVals可能是undefined,所以要加判断
const oldPageSize = oldVals?.[0]
const oldKeyword = oldVals?.[1]
const oldStatus = oldVals?.[2]
// 如果三个属性都没变,就不执行(刚挂载的时候immediate会触发,但oldVals是undefined,所以会执行)
if (oldPageSize === pageSize.value && oldKeyword === keyword.value && oldStatus === status.value) {
return
}
// 把标志位设为true
isResettingPage.value = true
// 重置page为1,并且保持其他参数不变
router.replace({
query: {
...route.query,
page: 1
}
})
// 这里注意!router.replace是异步的,所以要在下一个宏任务里把标志位设为false,并且执行请求
setTimeout(() => {
isResettingPage.value = false
debouncedFetchList()
}, 0)
},
{ immediate: false } // 刚挂载的时候不要触发,因为第一个watch已经触发了
)
这样修改之后,不管是改pageSize、keyword还是status,都会先重置page为1,然后只请求一次数据;改page的时候,直接请求一次数据,完美解决需求。
进阶场景2:路由回退时的特殊处理(比如保留滚动位置)
比如我们的列表页是长列表,用户滚动到第100条数据的位置,然后点进去看详情页,看完之后回退到列表页,这时候我们希望滚动位置自动还原,而不是回到顶部,这时候怎么处理?
这里可以配合Vue Router 4的scrollBehavior函数和sessionStorage来实现,首先在路由配置文件里设置scrollBehavior函数,当路由回退时,自动滚动到之前保存的位置:
// @/router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import List from '@/views/List.vue'
import Detail from '@/views/Detail.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/list', name: 'List', component: List },
{ path: '/detail/:id', name: 'Detail', component: Detail }
]
const router = createRouter({
history: createWebHashHistory(),
routes,
// scrollBehavior函数,接收to、from、savedPosition三个参数
scrollBehavior(to, from, savedPosition) {
// 如果有savedPosition,说明是路由回退,滚动到savedPosition
if (savedPosition) {
return savedPosition
} else {
// 否则滚动到顶部
return { top: 0 }
}
}
})
export default router
不过Vue Router 4的savedPosition只在使用浏览器的前进/后退按钮或者router.go(-1)/router.forward()的时候才会有,如果是用router.push('/list')或者router.replace('/list')跳转到列表页,savedPosition是undefined,而且如果我们的列表页是keep-alive缓存的,那组件不会重新mount,scrollBehavior会自动生效;但如果不是keep-alive缓存的,那组件会重新mount,接口会重新请求,数据会重新加载,这时候虽然scrollBehavior会执行,但列表还没渲染出来,滚动位置就会失效。
这时候怎么办?可以配合watch route.query和sessionStorage来实现:当列表页的scroll事件触发时,把当前的滚动位置保存到sessionStorage里,key是路由的path加上所有的query参数(这样不同的搜索/筛选/分页状态有不同的滚动位置);当组件重新mount或者watch route.query执行完请求之后,检查sessionStorage里有没有对应的滚动位置,如果有,就滚动到那个位置。
先修改列表页的template,给滚动容器加个ref(假设你的滚动容器是div,不是window):
<template>
<div class="list-container" ref="listContainerRef">
<div v-loading="loading" element-loading-text="加载中...">
<div v-for="item in list" :key="item.id" class="list-item">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
<router-link :to="`/detail/${item.id}`">查看详情</router-link>
</div>
</div>
</div>
</template>
然后修改列表页的script:
import { watch, ref, toRefs, onUnmounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getList } from '@/api/list'
import { debounce, throttle } from 'lodash-es'
const route = useRoute()
const router = useRouter()
const { page, pageSize, keyword, status } = toRefs(route.query)
const listContainerRef = ref(null)
const list = ref([])
const loading = ref(false)
const isResettingPage = ref(false)
// 生成sessionStorage的key的函数,key是路由的path加上所有的query参数
const generateScrollKey = () => {
return `scrollPosition_${route.path}_${JSON.stringify(route.query)}`
}
// 保存滚动位置的函数,用throttle包裹,防止频繁保存(比如每100毫秒保存一次)
const saveScrollPosition = throttle(() => {
if (listContainerRef.value) {
const scrollTop = listContainerRef.value.scrollTop
sessionStorage.setItem(generateScrollKey(), scrollTop)
}
}, 100)
const fetchList = async () => {
loading.value = true
try {
const params = {
page: parseInt(page.value) || 1,
pageSize: parseInt(pageSize.value) || 10,
keyword: keyword.value?.trim() || '',
status: status.value || 'all'
}
const res = await getList(params)
list.value = res.data.list
// 等列表渲染完成之后,检查sessionStorage里有没有对应的滚动位置,如果有,就滚动到那个位置
await nextTick()
const scrollTop = sessionStorage.getItem(generateScrollKey())
if (scrollTop && listContainerRef.value) {
listContainerRef.value.scrollTop = parseInt(scrollTop)
}
} catch (err) {
console.error('获取列表失败', err)
} finally {
loading.value = false
}
}
const debouncedFetchList = debounce(fetchList, 300)
onUnmounted(() => {
debouncedFetchList.cancel()
saveScrollPosition.cancel()
})
// 给滚动容器添加scroll事件监听
watch(
listContainerRef,
(newContainer) => {
if (newContainer) {
newContainer.addEventListener('scroll', saveScrollPosition)
}
},
{ immediate: true }
)
// 第一个watch:监听page变化
watch(
page,
() => {
if (!isResettingPage.value) {
debouncedFetchList()
}
},
{ immediate: true }
)
// 第二个watch:监听pageSize、keyword、status变化
watch(
[pageSize, keyword, status],
(newVals, oldVals) => {
const oldPageSize = oldVals?.[0]
const oldKeyword = oldVals?.[1]
const oldStatus = oldVals?.[2]
if (oldPageSize === pageSize.value && oldKeyword === keyword.value && oldStatus === status.value) {
return
}
isResettingPage.value = true
router.replace({
query: {
...route.query,
page: 1
}
})
setTimeout(() => {
isResettingPage.value = false
debouncedFetchList()
}, 0)
},
{ immediate: false }
)
这样修改之后,不管列表页是不是keep-alive缓存的,路由回退或者重新进入相同的搜索/筛选/分页状态时,滚动位置都会自动还原,体验感非常好。
Vue3 watch route query的正确打开方式
最后咱们总结一下Vue3 watch route query的正确打开方式,方便大家快速回顾:
- 别用生命周期组合拳和直接访问route.query,前者分散难维护,后者有浅层响应式的坑;
- 90%的场景用“万金油”写法:watch route.query + { deep: true, immediate: true },拿到query先转类型;
- 优化性能:用防抖解决频繁请求,用请求取消解决竞态问题,用toRef/toRefs精准监听指定属性;
- 处理进阶场景:用多个watch配合标志位处理部分变化,用scrollBehavior+sessionStorage处理路由回退时的滚动位置。
好了,今天关于Vue3 watch route query的内容就讲完了,从基础到进阶再到完整实战,所有坑都填实了,大家赶紧去自己的项目里试试吧!如果还有什么问题,欢迎在评论区留言哦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

