Vue Query 实例解析:轻松掌握核心用法

243 阅读14分钟

前置假设:

  • 你已经安装了 @tanstack/vue-query
  • 你已经在项目的入口文件(如 main.ts)中设置好了 QueryClientVueQueryPlugin(如之前介绍中所示)。
  • 你有一个发送 HTTP 请求的方式(这里以 axios 为例,但 fetch 或其他库同样适用)。假设 axios 已配置好,并在需要的地方导入。

示例 1:获取并展示列表数据 (例如:待办事项列表)

这是最基础的用法。

<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import axios from 'axios'; // 假设 axios 已配置

// 定义获取待办事项列表的函数
const fetchTodos = async () => {
  console.log('发起请求获取 Todos...'); // 方便观察请求时机
  const response = await axios.get('/api/todos');
  return response.data; // Vue Query 需要 Promise 返回实际数据
};

// 使用 useQuery Hook
const {
  data: todos,    // 响应式的 ref,持有获取到的数据 (或 undefined)
  status,       // 查询状态: 'pending', 'error', 'success'
  isLoading,   // 响应式的 ref: 初始加载时为 true (v5 前常用)
  isPending,    // 响应式的 ref: 初始加载时为 true (v5 推荐)
  isFetching,  // 响应式的 ref: 任何获取过程中都为 true (包括后台刷新)
  isError,     // 响应式的 ref: 获取失败时为 true
  error,       // 响应式的 ref: 持有错误对象
  refetch       // 手动触发重新获取的函数
} = useQuery({
  queryKey: ['todos'], // 必需:此查询的唯一标识键 (数组或字符串)
  queryFn: fetchTodos,  // 必需:用于获取数据的异步函数
  // 可选配置:如果数据不经常变动,可以增加 staleTime
  // staleTime: 5 * 60 * 1000, // 数据在 5 分钟内视为“新鲜”,不会因窗口聚焦等自动刷新
});
</script>

<template>
  <div>
    <h2>待办事项列表</h2>

    <!-- 加载状态 -->
    <div v-if="isPending">正在加载待办事项...</div>

    <!-- 错误状态 -->
    <div v-else-if="isError">发生错误: {{ error?.message }}</div>

    <!-- 成功状态 -->
    <ul v-else-if="todos">
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.title }} - {{ todo.completed ? '已完成' : '待办' }}
      </li>
    </ul>

    <!-- 无数据 -->
    <div v-else>暂无待办事项。</div>

    <!-- 后台刷新指示器 -->
    <div v-if="isFetching && !isPending" class="fetching-indicator">
      正在后台更新...
    </div>

    <!-- 手动刷新按钮 -->
    <button @click="refetch()" :disabled="isFetching">
      {{ isFetching ? '刷新中...' : '手动刷新' }}
    </button>
  </div>
</template>

<style scoped>
.fetching-indicator {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  background-color: lightblue;
  padding: 0.5rem;
  border-radius: 4px;
  font-size: 0.8em;
}
</style>

说明:

  • queryKey: ['todos']: 唯一标识了这个列表数据。如果其他地方也用这个 key 调用 useQuery,它们会共享缓存和状态。
  • queryFn: fetchTodos: 告诉 Vue Query 如何获取数据。
  • Vue Query 自动管理了 isPending, isError, data 等响应式状态,可以直接在模板中使用。
  • isFetching 表示任何获取过程,包括 Stale-While-Revalidate 或窗口聚焦触发的后台刷新。

示例 2:根据路由参数获取项目详情

这很常见,比如 /todos/1 显示 ID 为 1 的待办事项。

<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import { useRoute } from 'vue-router';
import { computed, watch } from 'vue';
import axios from 'axios';

const route = useRoute();

// 使用 computed 来响应式地获取路由参数 ID
const todoId = computed(() => route.params.id as string | undefined);

// 获取函数现在需要接收 ID
const fetchTodoById = async (id: string) => {
  // 注意:最好处理 id 不存在或无效的情况
  if (!id || Number.isNaN(parseInt(id, 10))) {
     // 可以返回 null, undefined, 或抛出错误,取决于你希望如何处理
     console.warn('无效的 Todo ID:', id);
     return null;
     // 或者 throw new Error('无效的 ID');
  }
  console.log(`发起请求获取 Todo ID: ${id}...`);
  const response = await axios.get(`/api/todos/${id}`);
  return response.data;
};

const {
  data: todo,
  isLoading, // 还是用 isLoading 吧,更清晰区分首次加载
  isError,
  error,
  status, // 可以直接用 status 判断更细致的状态
  // isFetching, // 如果需要后台刷新指示器也可以用
} = useQuery({
  // Query Key 现在包含动态 ID。
  // 当 todoId.value 变化时,Vue Query 会自动识别为不同的查询并获取数据!
  queryKey: ['todo', todoId],
  // queryFn 会接收一个包含 queryKey 的上下文对象,但直接用闭包访问 todoId 更方便
  queryFn: () => fetchTodoById(todoId.value!), // 传递当前的 ID
  // 只有当 ID 存在时才自动执行查询
  enabled: computed(() => !!todoId.value && !Number.isNaN(parseInt(todoId.value, 10))),
});

// (可选) 监听 status 或 error 变化来执行副作用,如提示
watch(status, (newStatus) => {
  console.log('Todo query status changed:', newStatus);
});
</script>

<template>
  <div>
    <h2>待办事项详情</h2>
    <div v-if="isLoading">正在加载详情...</div>
    <div v-else-if="isError">错误: {{ error?.message }}</div>
    <div v-else-if="todo">
      <h3>{{ todo.title }}</h3>
      <p>状态: {{ todo.completed ? '已完成' : '待办' }}</p>
      <p>ID: {{ todo.id }}</p>
    </div>
    <!-- 处理 fetchTodoById 返回 null 或 enabled 为 false 的情况 -->
    <div v-else-if="!todoId">请提供有效的待办事项 ID。</div>
    <div v-else>未找到该待办事项。</div>
  </div>
</template>

说明:

  • queryKey: ['todo', todoId]: 关键在于将响应式的 todoId (一个 computed ref) 放入 queryKey。当路由变化导致 todoId.value 更新时,Vue Query 会自动检测到 queryKey 的变化,并为新的 ID 获取数据(或使用该 ID 的缓存)。
  • enabled: computed(() => !!todoId.value && ...): 这是一个非常有用的选项,它确保只有当 todoId 有效时,查询才会自动执行,避免了在 ID 无效时发起不必要的 API 调用。

示例 3:添加项目 (Mutation + 缓存失效)

执行一个会改变数据的操作(如 POST 请求),然后更新列表视图。

<script setup lang="ts">
import { ref } from 'vue';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';

const queryClient = useQueryClient(); // 获取 QueryClient 实例,用于与缓存交互
const newTodoTitle = ref('');

// 定义执行变更的异步函数 (添加新的 todo)
const addTodoMutationFn = async (title: string) => {
  console.log('发起请求添加 Todo:', title);
  const response = await axios.post('/api/todos', { title, completed: false });
  return response.data;
};

// 使用 useMutation Hook
const {
  mutate: addTodo, // 调用此函数来触发变更操作 (不返回 Promise)
  // mutateAsync: addTodoAsync, // 如果需要 Promise,用这个
  status: addStatus,       // 变更的状态: 'idle', 'pending', 'error', 'success'
  isPending: isAddingTodo, // 是否正在执行变更
  isError: isAddError,
  error: addError,
  data: addedTodoData, // 变更成功后 mutationFn 返回的数据
} = useMutation({
  mutationFn: addTodoMutationFn,
  // --- 关键:成功回调 ---
  onSuccess: (newData, variables, context) => {
    // 变更成功!最常用的策略:使 'todos' 查询失效。
    // 这会告诉 Vue Query 与 ['todos'] 键关联的数据现在“不新鲜”了。
    // 所有活跃的 useQuery(['todos']) 实例会自动在后台重新获取数据。
    console.log('待办事项添加成功:', newData, ',原始变量:', variables);

    // 使列表查询失效
    queryClient.invalidateQueries({ queryKey: ['todos'] });

    // --- 另一种选择:手动更新缓存 (更即时,但可能更复杂) ---
    // 如果你想避免重新请求列表,可以手动更新缓存:
    // queryClient.setQueryData(['todos'], (oldData: any[] | undefined) => {
    //   // 确保 oldData 是数组
    //   const currentData = Array.isArray(oldData) ? oldData : [];
    //   return [...currentData, newData]; // 返回更新后的数组
    // });

    newTodoTitle.value = ''; // 清空输入框
  },
  // --- 错误处理 ---
  onError: (error, variables, context) => {
    console.error('添加待办事项失败:', error, ',提交的数据:', variables);
    // 可以在这里显示错误提示给用户
    alert(`添加失败: ${error.message}`);
  },
  // onSettled: (data, error, variables, context) => {
  //   // 无论成功或失败都会执行
  //   console.log('添加操作完成');
  // }
});

// 提交表单的处理函数
const submitNewTodo = () => {
  const title = newTodoTitle.value.trim();
  if (title) {
    addTodo(title); // 调用 mutate 函数,传入需要的数据
  }
};
</script>

<template>
  <div>
    <h3>添加新的待办事项</h3>
    <form @submit.prevent="submitNewTodo">
      <input type="text" v-model="newTodoTitle" placeholder="输入待办事项标题" :disabled="isAddingTodo" />
      <button type="submit" :disabled="isAddingTodo">
        {{ isAddingTodo ? '添加中...' : '添加' }}
      </button>
    </form>
    <div v-if="isAddError" style="color: red;">
      添加错误: {{ addError?.message }}
    </div>
    <!-- 列表组件 (如示例 1) 会因为 ['todos'] 查询失效并重新获取而自动更新 -->
  </div>
</template>

说明:

  • useMutation: 用于执行会改变服务器数据的操作。
  • mutationFn: 执行实际的 POST/PUT/DELETE 请求。
  • mutate: 调用这个函数来启动变更过程,并传入需要提交的数据。
  • onSuccess: 这是核心。变更成功后:
    • queryClient.invalidateQueries({ queryKey: ['todos'] }): 这是最常用也通常推荐的方式。它告诉 Vue Query ['todos'] 这个 key 的数据可能过期了。任何正在使用这个 key 的 useQuery 都会自动安排一次后台刷新,从而让列表 UI 更新。简单、可靠。
    • 注释掉的 setQueryData 展示了手动更新缓存的方式,可以提供更即时的 UI 反馈(无需等待刷新),但需要你精确地维护缓存数据结构。

补充说明:

  • mutate (即我们重命名后的 addTodo) 是一个由 useMutation 提供的、用来启动整个变更流程的【触发器函数】。
  • 当你调用 mutate(variables) 时,它会在内部安排并调用你提供的 addTodoMutationFn(variables) 这个异步函数。
  • 但是,mutate 函数本身的设计是**“发射后不管”(fire-and-forget)** 的风格,它不会等待 addTodoMutationFn 这个异步操作完成,因此不返回 Promise。它只是启动流程,然后立即返回 undefined

所以,可以这样理解:

  • mutate 启动了包含 addTodoMutationFn 执行在内的整个变更工作流(包括调用 onMutate, onSuccess, onError, onSettled 等)。
  • 间接调用addTodoMutationFn
  • 但它不直接等同于 addTodoMutationFn,并且刻意设计成不返回 Promise,以便调用它的地方(通常是 UI 事件处理)不会被异步操作阻塞。

与之对比的是 mutateAsync

  • mutateAsync (即我们例子中注释掉的 addTodoAsync) 也是由 useMutation 提供的触发器函数。
  • 它做的事情和 mutate 基本一样,也会启动整个变更流程并调用 addTodoMutationFn
  • 关键区别在于:mutateAsync 会返回一个 Promise。这个 Promise 会在整个变更流程(包括 mutationFn 和相关的回调)完成后解析 (resolve)(如果成功)或拒绝 (reject)(如果失败)。
  • 使用场景: 当你需要在触发变更操作后,基于其最终成功或失败的结果来执行一些后续逻辑时(例如,导航到不同页面、显示特定成功/失败消息等),使用 mutateAsync 并配合 await.then/.catch 会更方便。

简单总结:

  • mutate (addTodo): 像按下一个按钮,启动后台任务,按钮本身立即弹起(不等待任务完成)。用于简单触发,不关心后续结果的场景。
  • mutateAsync (addTodoAsync): 像按下一个按钮,启动后台任务,并且按钮会一直“按下”直到任务完成,然后告诉你结果(成功或失败)。用于需要根据任务最终结果执行后续操作的场景。

可以理解为“mutate 包装了下 addTodoMutationFn,但不返回 promise” 很好地概括了这个核心行为!


示例 4:删除项目 (Mutation + 乐观更新)

乐观更新通过在服务器确认前就更新 UI,来提供更流畅的用户体验。

<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';

// 假设这是 TodoItem 组件的一部分,接收一个 todo 对象作为 prop
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
const props = defineProps<{ todo: Todo }>();

const queryClient = useQueryClient();

// 定义删除操作的函数
const deleteTodoMutationFn = async (id: number) => {
  console.log(`发起请求删除 Todo ID: ${id}...`);
  // DELETE 请求通常不返回内容,或者只返回状态码
  await axios.delete(`/api/todos/${id}`);
};

// 使用 useMutation,配置乐观更新
const { mutate: deleteTodo, isPending: isDeleting } = useMutation({
  mutationFn: deleteTodoMutationFn,
  // --- 乐观更新逻辑 ---
  onMutate: async (idToDelete) => {
    console.log(`乐观更新:尝试删除 ID: ${idToDelete}`);
    // 1. 取消可能正在进行的针对 'todos' 列表的重新获取请求
    //    这可以防止它们覆盖我们的乐观更新。
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 2. 获取当前的 'todos' 列表缓存快照
    const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

    // 3. 乐观地从缓存中移除这一项
    if (previousTodos) {
      queryClient.setQueryData<Todo[]>(
        ['todos'], // 要更新的缓存 key
        // 更新函数:接收旧数据,返回新数据
        (old) => old?.filter(todo => todo.id !== idToDelete) ?? []
      );
      console.log('乐观更新:UI 上的列表已更新');
    } else {
      console.log('乐观更新:未找到缓存数据,无法进行乐观更新');
    }


    // 4. 返回一个包含快照的上下文对象,以便在出错时回滚
    return { previousTodos };
  },
  // --- 错误处理:回滚 ---
  onError: (err, idToDelete, context) => {
    console.error("删除失败,需要回滚:", err);
    // 如果 onMutate 中成功获取了快照,则用它来恢复缓存
    if (context?.previousTodos) {
      console.log('回滚 UI...');
      queryClient.setQueryData<Todo[]>(['todos'], context.previousTodos);
    }
    // 向用户显示错误信息
    alert(`删除失败: ${err.message}`);
  },
  // --- 最终处理:确保一致性 ---
  onSettled: (data, error, idToDelete, context) => {
    // 无论删除成功还是失败,最终都要重新从服务器获取一次 'todos' 列表
    // 来确保客户端状态与服务器状态最终一致。
    console.log(`删除操作完成 (成功或失败),重新验证 'todos' 列表`);
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

// 处理删除按钮点击
const handleDelete = () => {
  // 可以加一个确认框
  if (confirm(`确定要删除 "${props.todo.title}" 吗?`)) {
    deleteTodo(props.todo.id); // 调用 mutate 触发删除
  }
};
</script>

<template>
  <li>
    {{ todo.title }}
    <button @click="handleDelete" :disabled="isDeleting" style="margin-left: 10px;">
      {{ isDeleting ? '删除中...' : '删除' }}
    </button>
  </li>
</template>

说明:

  • 乐观更新 (Optimistic Update): 核心在于 onMutate。UI 会在用户点击删除后立即移除该项,感觉非常流畅。
  • onMutate:
    • cancelQueries: 防止后台刷新覆盖乐观更新。
    • getQueryData: 获取当前缓存。
    • setQueryData: 立即修改缓存,移除对应项。
    • 返回 context: 保存原始数据用于回滚。
  • onError: 如果服务器删除失败,使用 context.previousTodos 通过 setQueryData 将缓存恢复到原始状态。
  • onSettled: 极其重要。无论成功或失败,最后都要调用 invalidateQueries({ queryKey: ['todos'] })。这确保了客户端最终会与服务器的真实状态同步,修正了乐观更新可能带来的(暂时性)不一致。

这些示例覆盖了 Vue Query 最常用的一些场景。关键在于理解 queryKey 的作用、useQueryuseMutation 的基本用法,以及如何利用 QueryClient 的方法(尤其是 invalidateQueriessetQueryData)来管理缓存和状态更新。根据你的具体需求调整这些模式即可。

问题解答

1. useQuery 的使用位置

useQuery 必须在组件的 setup 函数中调用,或者在能够访问 Vue 的依赖注入上下文的地方(例如自定义的 Composition API 函数中,这个函数本身也是在 setup 内被调用的)。

例如,在 Vue 3 的 <script setup> 中:

<script setup>
import { useQuery } from '@tanstack/vue-query';

// 在这里使用 useQuery 是正确的
const { data, isLoading, error } = useQuery({
  queryKey: ['myData'],
  queryFn: fetchDataFunction,
});

async function fetchDataFunction() {
  // 你的异步数据请求逻辑
  const response = await fetch('/api/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}
</script>

2. 避免重复请求和实现数据共享 (低频更新)

VueQuery 的核心优势之一就是它的缓存机制。要实现你的需求(低频更新接口,不同页面共享数据,避免不必要的重复请求),关键在于正确配置 useQuery 的选项,特别是 queryKeystaleTime

  • queryKey: 这是一个数组,用作缓存的唯一标识符。在所有需要共享这份数据的组件中,务必使用完全相同的 queryKey。例如 ['sharedApiData']

  • staleTime: 这个选项指定了数据在被视为“过时” (stale) 之前可以保持“新鲜” (fresh) 的时长(单位:毫秒)。

    • 在数据被视为新鲜期间,即使组件重新挂载或 useQuery 实例重新创建 (例如切换页面),VueQuery 也会直接从缓存中提供数据,而不会发起新的网络请求。
    • 对于更新频率很低的接口,你可以将 staleTime 设置为一个较大的值,比如几分钟、几小时,甚至 Infinity (如果数据几乎永不改变,只在手动触发时更新)。
  • cacheTime: 这个选项定义了数据在从缓存中被垃圾回收之前可以保留的时长(单位:毫秒)。即使数据已经 "stale",只要它还在缓存中 (未超过 cacheTime) 并且没有活动的 useQuery 实例监听它,它就会被保留。默认是 5 分钟。如果你的 staleTime 设置得非常长 (比如 Infinity),那么 cacheTime 也应该相应设置得足够长,否则 "fresh" 的数据可能会因为缓存过期而被清除。通常,cacheTime 应大于等于 staleTime。VueQuery 内部如果 staleTimecacheTime 大,它会表现得像 cacheTimestaleTime 一样长。为了简单起见,如果你设置了很长的 staleTime,也设置一个同样长的 cacheTime

  • 其他相关选项:

    • refetchOnMount: 默认为 true。如果数据是 stale 的,组件挂载时会重新获取。如果 staleTime 设置得足够长,数据会长时间保持 fresh,即使这个选项是 true,也不会在挂载时重新获取。如果希望更严格地控制,可以设为 false,这样只有在数据不存在或明确需要时才获取。
    • refetchOnWindowFocus: 默认为 true。窗口重新获得焦点时会重新获取 stale 数据。对于低频更新的数据,可以设为 false
    • refetchOnReconnect: 默认为 true。网络重连后会重新获取 stale 数据。同样,可以设为 false

解决方案示例:

假设你有一个获取用户配置的接口,这个配置很少变动。

在你的 Vue 组件中 (PageA.vue, PageB.vue 等):

<script setup>
import { useQuery } from '@tanstack/vue-query';

const QUERY_KEY_USER_CONFIG = ['userConfig'];

// 模拟 API 请求函数
async function fetchUserConfig() {
  console.log('Fetching user config from API...'); // 观察这个日志的打印次数
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 1000));
  return {
    id: 1,
    name: 'Test User',
    preferences: {
      theme: 'dark',
      notifications: true,
    },
    lastUpdated: new Date().toLocaleTimeString(),
  };
}

const { data: userConfig, isLoading, isError, error } = useQuery({
  queryKey: QUERY_KEY_USER_CONFIG, // 关键:所有页面使用相同的 queryKey
  queryFn: fetchUserConfig,
  staleTime: 1000 * 60 * 30, // 数据在30分钟内是新鲜的
  // cacheTime: 1000 * 60 * 35, // 缓存35分钟 (确保 >= staleTime)
                                // VueQuery 默认 cacheTime 是 5 分钟。如果 staleTime 比 cacheTime 长,
                                // query 会在 staleTime 之后才 inactive,然后 cacheTime 开始计时。
                                // 如果你希望在数据变为 stale 后,还能在缓存中保留更久,可以显式设置更长的 cacheTime。
                                // 对于 staleTime: Infinity 的情况,cacheTime 也应该设为 Infinity 或足够长。
  refetchOnWindowFocus: false, // 关闭窗口聚焦时重新获取
  refetchOnMount: false,       // 关闭挂载时重新获取 (如果数据已存在且新鲜,即使true也不会获取;设为false更保险)
  refetchOnReconnect: false,   // 关闭网络重连时重新获取
});
</script>

<template>
  <div>
    <div v-if="isLoading">Loading user config...</div>
    <div v-else-if="isError">Error fetching config: {{ error?.message }}</div>
    <div v-else-if="userConfig">
      <h2>User Configuration ({{ userConfig.lastUpdated }})</h2>
      <pre>{{ userConfig }}</pre>
    </div>
  </div>
</template>

说明:

  1. 一致的 queryKey: ['userConfig'] 在所有使用此数据的页面中保持一致。
  2. staleTime: 1000 * 60 * 30: 数据获取后,30分钟内被认为是新鲜的。当你从一个页面切换到另一个使用相同 queryKey 的页面时,只要还在30分钟内,fetchUserConfig 就不会被再次调用,useQuery 会直接返回缓存中的数据。
  3. refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false: 这些设置进一步减少了自动重新获取数据的机会,更符合“低频更新”的需求。

全局配置 (推荐):

你还可以在创建 QueryClient 时设置默认的查询选项,这样就不需要在每个 useQuery 中重复配置。

// main.js 或 main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query';

const app = createApp(App);

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 默认staleTime为5分钟
      refetchOnWindowFocus: false,
      // ... 其他全局默认配置
    },
  },
});

app.use(VueQueryPlugin, { queryClient }); // 或者 app.use(VueQueryPlugin, { queryClientConfig: { defaultOptions: { ... } } })

app.mount('#app');

然后,在组件中 useQuery 时,除非你想覆盖全局配置,否则它会使用这些默认值。

对比 Pinia/自定义 Hook 的“有无数据”逻辑:

VueQuery 的 staleTime 和缓存机制,结合其自动管理数据状态(isLoading, isFetching, isSuccess, isError, data 等),本身就优雅地处理了“判断有无数据,无数据接口请求”的逻辑。

  • useQuery 首次为一个 queryKey 执行时,或者当缓存数据已不存在或已 stale 且需要 refetch 时,它会调用 queryFn
  • 如果缓存中有对应 queryKey 的 fresh 数据,它会直接使用缓存数据,queryFn 不会执行。

这种方式比手动在 Pinia store 或自定义 hook 中检查数据是否存在然后决定是否调用 API 更加声明式和强大,因为它还内置了后台更新、重试、依赖查询等高级功能。

通过上述配置,你应该能够在不同页面间高效共享低频更新的接口数据,避免不必要的请求。切换页面时,只要数据在 staleTime 内,console.log('Fetching user config from API...'); 应该不会频繁打印。

3. refetchOnReconnect参数详解

refetchOnReconnect 设置为 true(默认值)时,如果网络断开后重新连接,Vue Query 会尝试重新获取那些当前处于活动状态 (active) 并且数据状态为 "stale" (过时) 的查询

这里有几个关键点需要理解:

  1. 活动状态 (Active Queries)

    • 一个查询被认为是“活动的”,如果它至少有一个正在被使用的 useQuery 实例(即,相关的组件当前是挂载的,并且正在监听这个查询)。
    • 如果一个查询没有活动的实例(比如所有使用该 queryKey 的组件都已卸载),那么即使 refetchOnReconnect: true,它在网络重连时通常也不会被自动重新获取,直到它再次变为活动状态。
  2. 数据状态为 "Stale" (过时)

    • 只有当查询的数据被认为是 "stale" 时,refetchOnReconnect: true 才会触发重新获取。
    • 数据是否 "stale" 取决于你的 staleTime 配置。如果 staleTime 设置得非常长(例如 Infinity),那么数据会长时间保持 "fresh"(新鲜)状态。在这种情况下,即使网络重连,并且 refetchOnReconnect: true,由于数据仍然是 "fresh",Vue Query 也不会发起新的请求。
  3. "所有地方"的含义

    • 它不是指你代码库中定义过的所有 useQuery,而是指那些在网络重连那一刻,同时满足“活动状态”和“数据已过时”这两个条件的查询
    • 所以,如果你的应用中有多个组件使用了不同的 queryKey,并且它们在网络重连时都是活动的,并且它们各自的数据都已过时,那么它们都会尝试重新获取。

总结一下:

  • 会重新请求:那些在网络重连时,其关联组件是挂载的(查询是活动的),并且它们的数据已经超过了 staleTime(数据是 stale 的)。
  • 不会重新请求
    • 那些数据仍然是 "fresh" 的查询(未超过 staleTime)。
    • 那些没有活动实例的查询(例如,相关组件已卸载)。

如何控制这种行为?

  • 全局设置:你可以在创建 QueryClient 时,为所有查询设置默认的 refetchOnReconnect 行为:

    // main.js 或 main.ts
    import { QueryClient } from '@tanstack/vue-query';
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          refetchOnReconnect: false, // 全局禁用网络重连时自动重新获取
          // 或者设置为 true (默认), 或者一个函数 (()=>{...}) 来自定义逻辑
        },
      },
    });
    
  • 单个查询设置:你也可以在每个 useQuery 中单独覆盖这个设置:

    <script setup>
    import { useQuery } from '@tanstack/vue-query';
    
    useQuery({
      queryKey: ['myData'],
      queryFn: fetchData,
      refetchOnReconnect: false, // 对这个特定查询禁用
    });
    </script>
    

对于你的场景,如果接口更新频率很低,并且你不希望网络波动或短暂断开后立即触发大量请求,那么将 refetchOnReconnect 设置为 false (无论是全局还是针对特定查询) 可能是一个更合适的选择。这样,数据将仅根据 staleTimerefetchOnMountrefetchOnWindowFocus 等其他配置或手动操作来更新。