Vue3 + vue-query 的重复请求问题解决记录

前言

@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 (即 useQueryrefetch) 的地方。

当时的 script setup 核心逻辑:

javascript 复制代码
// ...
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次请求的来源:

  1. 第一次 (useQuery 自动触发) : useQuery 在组件挂载时,会使用初始的 queryKey 自动发起一次请求。
  2. 第二次 (onMounted 手动触发) : onMounted 钩子中明确调用了 loadData()
  3. 第三次 (watch(activeTab)) : activeTab 在初始化时,watch 监听器被触发,调用了 loadData()
  4. 第四次 (PageWrapper 初始化) : 封装的 <cec-page-wrapper> 组件在内部的分页器初始化时,会 emit 一次 @load-data 事件,再次调用了 loadData()

根源 : 我们犯了一个典型的错误------不信任框架,手动控制一切 。我们试图在每个可能的地方都调用 loadData,却没有意识到 useQuery 的响应式 queryKey 已经为我们处理了大部分情况,从而导致了多次重复的调用。

解决方案 (V1) : 我们决定信任 useQuery,并精确控制请求时机。

  1. 禁用自动请求 : 给 useQuery 添加 enabled: false 选项。
  2. 添加 isMounted 守卫 : 创建一个 isMounted 标志位,阻止分页组件在 onMounted 完成前的初始化调用。
  3. 统一入口 : 在 onMounted最后 ,手动调用 refetch() 来发起唯一的第一次请求。
javascript 复制代码
// ...
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

更深层次的根源 : useQueryqueryKey 依赖于 searchParams。而 searchParams 的初始值是通过 cloneDeep(activeFilters.value) 计算得来的。activeFilters 又依赖 filterParamsactiveTab。整个依赖链条是这样的:

activeTab -> activeFilters -> searchParams -> queryKey

在组件挂载的微任务队列中,这些响应式数据的初始化和 watch 的触发顺序存在我们未预料到的交互,导致了 searchParams 在短时间内被更新了两次。

最终的解决方案 (V2) : 我们意识到,问题的关键不是去"堵"住所有的触发点,而是让触发机制变得单一和可预测

  1. 回归 useQuery 的自动行为 : 我们移除 enabled: false,我们相信并利用它的自动加载能力。
  2. 确保初始 queryKey 的稳定性 : 最重要的修改------在定义 searchParams 时,就立即 用稳定的初始值 cloneDeep(activeFilters.value) 对其进行初始化。
  3. 移除所有手动的首次加载调用 : 删除 onMounted 中的 refetch()search() 调用。

最终只有1次请求的代码:

javascript 复制代码
// 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 调用!
});

为什么这次能成功?

  • 单一入口 : useQueryqueryFn 现在是获取数据的唯一入口。
  • 单一触发器 : queryKey 的变化是触发 queryFn 的唯一方式。
  • 可预测的状态 : 在组件挂载时,searchParams 的初始值是确定的、稳定的。useQuery 使用这个稳定的 key 发起了唯一一次初始请求。
  • 清晰的职责 :
    • 用户的输入 只改变 activeFilters (UI状态)。
    • 用户的搜索动作 (@search, @change, reset) 才去调用 search(),将 UI 状态"提交"给 searchParams (查询状态)。
    • useQuery 忠实地响应 searchParams 的变化。

结论

还是要深入了解 Vue 3 响应式系统和 vue-query 声明式数据获取的理念啊~~~不然有AI也要卡壳子,耽误牛马下班~

  • 不要与框架对抗 : 相信 useQuery 的响应式能力,避免在 onMountedwatch 中进行手动的、命令式的 refetch 调用。
  • 分离状态 : 将用于UI双向绑定的状态(如 activeFilters)和用于触发数据请求的状态(如 searchParams)分离开,可以有效切断意外的响应式连锁反应。
  • 稳定 queryKey : 确保 useQuery 在首次自动执行时,其 queryKey 所依赖的所有数据都已处于稳定和正确的初始状态。

通过遵循这些原则,我们可以构建出既简洁又健壮的数据获取逻辑,真正发挥出现代前端框架的威力。

相关推荐
不知名程序员第二部7 小时前
前端-业务-架构
前端·javascript·代码规范
Bug生产工厂7 小时前
React支付组件设计与封装:从基础组件到企业级解决方案
前端·react.js·typescript
小喷友7 小时前
阶段三:进阶(Rust 高级特性)
前端·rust
华仔啊7 小时前
面试官:请解释一下 JS 的 this 指向。别慌,看完这篇让你对答如流!
前端·javascript
Strayer7 小时前
Tauri2.0打包构建报错
前端
小高0077 小时前
💥💥💥前端“隐藏神技”:15 个高效却鲜为人知的 Web API 大起底
前端·javascript
flyliu7 小时前
再再次去搞懂事件循环
前端·javascript
艾小码7 小时前
还在拍脑袋估工时?3个技巧让你告别加班和延期!
前端·敏捷开发
UrbanJazzerati7 小时前
前端入门:vh、padding、margin、outline、pointer-events
前端·面试