Vue3 + vue-query 的重复请求问题解决
作者:CassieHuu
前言
@tanstack/vue-query
以其强大的缓存和状态管理能力,极大地简化了数据获取逻辑。然而,当它与 Vue3 复杂的响应式系统结合时,如果不深入理解其工作原理,很容易陷入“重复请求”的陷阱。在最近一次开发中就遇到了这个问题,本文展现了从一个页面加载时触发4次重复API请求,到2次,并最终实现1次请求的解决过程,以免下次再掉进这个坑里。
问题的初现:一个“勤奋过头”的列表页
我们有一个需求:开发一个包含Tabs切换、独立筛选条件和分页功能的数据列表页面。技术栈是 Vue 3 (Composition API) 和 @tanstack/vue-query
,并使用了一个名为 <cec-page-wrapper>
的高度封装的表格组件。
页面完成后,一切似乎工作正常,但打开浏览器网络面板,我们惊恐地发现,每次加载页面,获取列表数据的API transactionList
竟然被调用了4次!
发生了什么???
第一阶段:从4次请求到2次 —— 寻找“幽灵触发者”
我们首先审查了代码,试图找出所有可能调用 loadData
(即 useQuery
的 refetch
) 的地方。
当时的 script setup 核心逻辑:
// ... const activeTab = ref('PROVIDER'); const tablePage = ref({ currentPage: 1, ... }); const filterParams = ref({ ... }); const { isFetching, data: tableData, refetch: loadData } = useQuery({ queryKey: ['transactionList', activeTab, tablePage, filterParams], queryFn: async () => { /* ... */ }, }); watch(activeTab, () => { tablePage.value.currentPage = 1; loadData(); // Tab 切换时加载 }); onMounted(() => { getBusinessNodesList(); // 获取下拉框选项 loadData(); // 挂载时加载 }); // 模板中,分页组件 @change 事件也调用了 loadData
经过仔细的日志打印和分析,我们定位到了这4次请求的来源:
- 第一次 (useQuery 自动触发): useQuery 在组件挂载时,会使用初始的 queryKey 自动发起一次请求。
- 第二次 (onMounted 手动触发): onMounted 钩子中明确调用了 loadData()。
- 第三次 (watch(activeTab)): activeTab 在初始化时,watch 监听器被触发,调用了 loadData()。
- 第四次 (PageWrapper 初始化): 封装的 <cec-page-wrapper> 组件在内部的分页器初始化时,会 emit 一次 @load-data 事件,再次调用了 loadData()。
根源: 我们犯了一个典型的错误——不信任框架,手动控制一切。我们试图在每个可能的地方都调用 loadData
,却没有意识到 useQuery
的响应式 queryKey
已经为我们处理了大部分情况,从而导致了多次重复的调用。
解决方案 (V1): 我们决定信任 useQuery
,并精确控制请求时机。
- 禁用自动请求: 给 useQuery 添加 enabled: false 选项。
- 添加 isMounted 守卫: 创建一个 isMounted 标志位,阻止分页组件在 onMounted 完成前的初始化调用。
- 统一入口: 在 onMounted 的最后,手动调用 refetch() 来发起唯一的第一次请求。
// ... const { ..., refetch } = useQuery({ ..., enabled: false }); const isMounted = ref(false); const handlePageChange = (pageInfo) => { if (!isMounted.value) return; // 守卫 // ... refetch(); }; onMounted(async () => { await getBusinessNodesList(); searchParams.value = cloneDeep(activeFilters.value); await refetch(); // 手动触发 isMounted.value = true; });
结果: 这个修改非常有效!请求次数从4次锐减到了2次。但为什么还有2次?
第二阶段:从2次请求到1次 —— 深入响应式依赖
我们再次审查代码,发现 onMounted 中手动调用的 refetch() 仍然是多余的。尽管我们禁用了它,但 watch 和其他地方的逻辑仍然可能在初始化阶段触发 refetch。
更深层次的根源: useQuery 的 queryKey 依赖于 searchParams。而 searchParams 的初始值是通过 cloneDeep(activeFilters.value) 计算得来的。activeFilters 又依赖 filterParams 和 activeTab。整个依赖链条是这样的:
activeTab -> activeFilters -> searchParams -> queryKey
在组件挂载的微任务队列中,这些响应式数据的初始化和 watch 的触发顺序存在我们未预料到的交互,导致了 searchParams 在短时间内被更新了两次。
最终的解决方案 (V2): 我们意识到,问题的关键不是去“堵”住所有的触发点,而是让触发机制变得单一和可预测。
- 回归 useQuery 的自动行为: 我们移除 enabled: false,我们相信并利用它的自动加载能力。
- 确保初始 queryKey 的稳定性: 最重要的修改——在定义 searchParams 时,就立即用稳定的初始值 cloneDeep(activeFilters.value) 对其进行初始化。
- 移除所有手动的首次加载调用: 删除 onMounted 中的 refetch() 或 search() 调用。
最终只有1次请求的代码:
// 1. 状态定义:searchParams 在定义时就拥有了正确的初始值 const activeTab = ref('PROVIDER'); const activeFilters = computed(() => filterParams[activeTab.value]); const searchParams = ref(cloneDeep(activeFilters.value)); // 关键! // 2. useQuery: 默认启用,它会在挂载时自动使用上面的 searchParams 发起请求 const { isFetching, data: tableData, refetch } = useQuery({ queryKey: computed(() => ['transactionList', activeTab.value, ..., searchParams.value]), queryFn: async () => { /* ... */ }, }); // 3. 事件处理:只负责更新状态,不直接调用 refetch const search = () => { activeTableState.value.currentPage = 1; // 只更新 searchParams,useQuery 会自动响应 queryKey 的变化 searchParams.value = cloneDeep(activeFilters.value); }; // 4. 生命周期:只做与列表数据无关的初始化 onMounted(() => { getBusinessNodesList(); // 不需要任何 loadData 或 refetch 调用! });
为什么这次能成功?
- 单一入口: useQuery 的 queryFn 现在是获取数据的唯一入口。
- 单一触发器: queryKey 的变化是触发 queryFn 的唯一方式。
- 可预测的状态: 在组件挂载时,searchParams 的初始值是确定的、稳定的。useQuery 使用这个稳定的 key 发起了唯一一次初始请求。
- 清晰的职责:
- 用户的输入只改变 activeFilters (UI状态)。
- 用户的搜索动作 (@search, @change, reset) 才去调用 search(),将 UI 状态“提交”给 searchParams (查询状态)。
- useQuery 忠实地响应 searchParams 的变化。
结论
还是要深入了解 Vue 3 响应式系统和 vue-query
声明式数据获取的理念啊~~~不然有AI也要卡壳子,耽误牛马下班~
- 不要与框架对抗: 相信
useQuery
的响应式能力,避免在onMounted
或watch
中进行手动的、命令式的refetch
调用。 - 分离状态: 将用于UI双向绑定的状态(如
activeFilters
)和用于触发数据请求的状态(如searchParams
)分离开,可以有效切断意外的响应式连锁反应。 - 稳定
queryKey
: 确保useQuery
在首次自动执行时,其queryKey
所依赖的所有数据都已处于稳定和正确的初始状态。
通过遵循这些原则,我们可以构建出既简洁又健壮的数据获取逻辑,真正发挥出现代前端框架的威力。
到此这篇关于Vue3 + vue-query 的重复请求问题解决 的文章就介绍到这了,更多相关vue-query 重复请求内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!