Vue Query 并行查询(Parallel Queries)完整指南:从 useQuery 并排调用到 useQueries 动态并行
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
本指南围绕 TanStack Vue Query 的并行查询能力展开,介绍在 Vue 3 组合式 API 场景下如何同时发起多个请求以最大化抓取并发度:数量固定的场景直接并排使用useQuery即可,数量随渲染动态变化的场景则需要useQueries组合式函数。读完本文你将掌握useQueries的完整参数、响应式与 combine 机制、TypeScript 类型推断技巧,以及其底层QueriesObserver的实现原理。
什么是并行查询
并行查询(Parallel Queries)指在同一时间同时执行的查询,目的是让多个相互独立的数据请求并发发起,从而最大化抓取并发度,避免串行等待造成的整体加载时间拉长。
在 Vue Query 中,实现并行查询有两种方式:
- 手动并行:直接并排使用多个
useQuery(或useInfiniteQuery)组合式函数; - 动态并行:使用
useQueries组合式函数,根据运行时数据动态生成任意数量的查询。
本指南对应的 Vue 端核心文档为 docs/framework/vue/guides/parallel-queries.md,其内容来源于 React 侧的官方指南 docs/framework/react/guides/parallel-queries.md,并在 Vue 响应式体系下做了适配。
手动并行查询:数量固定时的最简单方案
当并行查询的数量在运行期间保持不变时,使用并行查询不需要任何额外工作——只需并排使用任意数量的useQuery(或useInfiniteQuery)即可。以下是一个标准的 Vue 3<script setup>示例:
<script setup lang="ts"> // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects, }) </script>这三个查询在组件创建时会被同时发起,互不阻塞。useQuery的签名定义于 packages/vue-query/src/useQuery.ts:它接收一个UseQueryOptions(其中queryKey支持MaybeRef,其余选项大多支持MaybeRefDeep),返回响应式的查询状态。由于 Vue 的setup()每个组件实例只执行一次,这种并排写法天然安全,不存在 "hooks 规则" 的限制。
Suspense 模式提示:在 React Query 的 Suspense 模式下,这种并排写法会因为第一个查询抛出 Promise 挂起组件而失效,需要改用
useSuspenseQueries。在 Vue 侧,useQuery本身是同步返回响应式状态、不依赖 Suspense 抛 Promise 的机制,因此该限制主要影响 React 生态;如果使用 Vue 的<Suspense>组合异步组件,仍建议让每个查询组件各自独立,避免整体挂起。
动态并行查询:数量可变时使用 useQueries
如果需要的查询数量会随渲染状态变化(例如来自接口返回的用户列表,每个用户要拉取一份详情),就不能再手写固定数量的useQuery。Vue Query 提供了useQueries组合式函数,可以动态地并行执行任意数量的查询,且完全遵循 Vue 的响应式体系。
useQueries接收一个选项对象,其中包含一个queries 键,其值是一个查询对象数组;它返回一个只读 ref,.value是查询结果数组,顺序与输入顺序一致:
<script setup lang="ts"> import { computed } from 'vue' const users = computed(...) const queries = computed(() => users.value.map(user => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }) ); const userQueries = useQueries({queries: queries}) </script>上面的例子中,users变化后,queries这个 computed 会重新求值,useQueries会立刻为新生成的每个用户查询对象创建对应的查询并并行发起请求,返回的userQueries数组也会同步更新——这正是动态并行查询的核心价值。
参数详解
根据 Vue 侧参考文档 docs/framework/vue/reference/useQueries.md 与实现源码,useQueries接受如下选项:
| 选项 | 类型 | 说明 |
|---|---|---|
queries | (() => MaybeRefDeep<UseQueriesOptions>) \| MaybeRefDeep<UseQueriesOptions> | 查询对象数组,或返回数组的函数;数组内每个元素的选项与useQuery的选项一致,但不包含queryClient(它需要在顶层单独传入) |
combine | (result: UseQueriesResults) => TCombinedResult | 可选。将多个查询结果合并为单个值返回 |
queryClient | QueryClient | 第二个参数。传入自定义QueryClient,否则使用最近上下文中的客户端 |
需要注意:
placeholderData:useQueries同样支持placeholderData选项,但它不像useQuery那样能拿到"上一次渲染的查询结果"作为占位,因为每次渲染传入的查询数量可能不同,无法建立一一对应的映射关系。- 重复 queryKey 警告:如果查询对象数组中同一个 queryKey 出现多次,可能会导致数据在查询之间共享(去重合并)。需要去重时,应先对查询做去重,再手动把结果映射回期望的结构。
源码层面的响应式实现
useQueries的实现位于 packages/vue-query/src/useQueries.ts,其核心流程如下:
- 响应式解析查询列表:用一个
computed对queries求值——支持函数形式(() => queries)和 ref 形式(unref顶层数组),并对每个查询对象调用cloneDeepUnref深层解包其中的 ref;如果enabled是函数(getter),则在此处立即调用取值。 - 统一默认选项:每个查询对象都经过
client.defaultQueryOptions(clonedOptions)处理,补全默认的staleTime、retry、refetchOnWindowFocus等配置,并设置_optimisticResults。 - 委托给 QueriesObserver:创建一个来自
@tanstack/query-core的QueriesObserver,订阅后每次状态变化都会更新内部的shallowRef状态。 - 监听变化:通过
watch(defaultedQueries, ...)在查询列表变化时调用observer.setQueries(...)并刷新乐观结果;通过onScopeDispose在组件卸载时取消订阅,避免内存泄漏。 - 返回只读 ref:根据
options.shallow决定返回shallowReadonly(state)还是readonly(state)。
此外,在开发模式下,如果在setup()或运行中的 effect scope 之外调用useQueries,源码会打印警告:"vue-query composable likeuseQuery()should only be used inside asetup()function or a running effect scope. They might otherwise lead to memory leaks."。对应的测试用例见 packages/vue-query/src/tests/useQueries.test.ts。
返回结果与单独 refetch
useQueries返回的数组中,每个元素都是标准查询结果(QueryObserverResult,包含status、data、error、isPending、isFetching、isStale等字段),并且每个结果还挂载了独立的refetch方法。从源码 packages/vue-query/src/useQueries.ts 可以看到,refetch会通过observer.getOptimisticResult定位到对应下标的查询,再调用其query.refetch——也就是说可以只重拉某一个查询,而不影响其他并行查询。
测试用例 packages/vue-query/src/tests/useQueries.test.ts 验证了这一点:对queriesState.value[0].refetch()后,只有第 0 个查询的数据更新,第 1 个查询的数据保持不变。
响应式更新与 getter 支持
useQueries的queries选项支持多种响应式形态,测试用例覆盖了:
- ref 数组:
queries = ref([...]),修改queries.value(如 splice 替换)后,useQueries会同步返回新查询列表对应的结果(见 测试用例); - queryKey 中的 getter:queryKey 内可以嵌入
() => ref.value形式的 getter,ref 变化时自动触发重新抓取; - 任意深度嵌套 getter:queryKey 中支持任意嵌套的 ref / getter 组合(数组、对象、函数均可);
- options getter 函数:
queries: () => [...]函数形式同样具备响应式。
这些能力源于MaybeRefDeep类型与cloneDeepUnref实现,保证了 Vue 生态中"ref 即响应式来源"的使用习惯在并行查询中完全成立。
使用 combine 合并查询结果
如果希望把多个查询的data(或其他信息)合并成单个值返回,可以使用combine选项:
const ids = [1, 2, 3] const combinedQueries = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), })), combine: (results) => { return { data: results.map((result) => result.data), pending: results.some((result) => result.isPending), } }, })上述示例中,combinedQueries.value是一个包含data和pending两个属性的对象。注意:所有其他查询结果属性(如error、isFetching等)都会丢失,需要什么就在combine返回值中显式保留。
从 Vue 侧参考文档 docs/framework/vue/reference/useQueries.md 可知,combine的结果会进行结构共享(structural sharing),尽可能保持引用稳定。底层实现在 packages/query-core/src/queriesObserver.ts:combine只在以下情况重新执行——
combine函数本身引用发生变化;- 任意一个查询结果发生变化。
Vue 中的 memoization 说明
与 React 不同,Vue 的setup()函数每个组件实例只运行一次,因此上面这种内联定义的combine函数在响应式更新期间已经拥有稳定引用,无需额外的 memoization。直接内联即可享受结构共享带来的引用稳定性收益。
测试用例 packages/vue-query/src/tests/useQueries.test.ts 验证了combine行为:两个查询成功后,queriesResult.value等于{ combined: true, res: [firstResult, secondResult] }。
TypeScript:内联 select 的类型推断限制与解决
与useQuery不同,useQueries无法从同一个查询对象中的queryFn推断出内联select的data参数类型。原因在于useQueries一次性推断整个queries数组的类型,内联写在对象字面量里的select无法从同对象的queryFn获得上下文类型,因此回退为unknown(这是 TanStack Query 已知的 TypeScript 限制)。
useQueries({ queries: [ { queryKey: ['post', 1], queryFn: () => fetchPost(1), // ❌ 这里的 `data` 是 `unknown` select: (data) => data.title, }, ], })官方提供两种解决方案:
方案一:显式标注select参数类型
useQueries({ queries: [ { queryKey: ['post', 1], queryFn: () => fetchPost(1), // ✅ 这里的 `data` 是 `Post` select: (data: Post) => data.title, }, ], })方案二:先用queryOptions定义查询,再传入useQueries
queryOptions帮助函数会在单个对象内部先解析好类型,再交给useQueries:
const postOptions = (id: number) => queryOptions({ queryKey: ['post', id], queryFn: () => fetchPost(id), // ✅ 这里的 `data` 是 `Post` select: (data) => data.title, }) useQueries({ queries: [postOptions(1), postOptions(2)] })同样的限制也适用于展开覆盖:对queryOptions结果做展开(spread)再覆盖select时,覆盖的select依然会回退为unknown:
useQueries({ queries: [ { ...postOptions(1), // ❌ 这里的 `data` 是 `unknown` select: (data) => data.title, }, ], })解决办法是再次用queryOptions包裹展开结果,让覆盖发生在类型解析之前:
useQueries({ queries: [ queryOptions({ ...postOptions(1), // ✅ 这里的 `data` 是 `Post` select: (data) => data.title, }), ], })类型层面的实现佐证
useQueries的类型系统相当严谨,见 packages/vue-query/src/useQueries.ts:
UseQueriesOptions会递归地逐项解包查询对象,推断并强制校验泛型参数(queryFnData、error、data、queryKey),并设置MAXIMUM_DEPTH = 20防止大型数组字面量触发 TS 深度限制;UseQueriesResults递归地把每个选项映射为对应的QueryObserverResult,并通过GetDefinedOrUndefinedQueryResult处理initialData的有无,从而区分DefinedQueryObserverResult与普通QueryObserverResult;- 这两个工具类型均从 packages/vue-query/src/index.ts 导出,供库使用者组合自己的类型。
这意味着使用queryOptions或元组显式标注(如[TQueryFnData, TError, TData])时,返回的结果数组类型也能被精确推导。
底层原理:QueriesObserver 如何调度并行查询
useQueries的上层是 Vue 响应式封装,真正负责调度查询的是@tanstack/query-core中的QueriesObserver,实现位于 packages/query-core/src/queriesObserver.ts。
从源码结构看,它的工作方式可以概括为:
- 构造时接收查询数组,为每个查询创建独立的
QueryObserver,并分别订阅其状态变化(见 queriesObserver.ts); - 提供
setQueries方法,在查询列表变化时增量同步:复用 queryKey 未变的 observer,销毁移除的 observer,为新查询创建 observer(见 queriesObserver.ts); - 提供
getOptimisticResult以支持渲染期乐观结果(在 Vue 中用于shallowRef的初始值与订阅后的首帧同步,见 queriesObserver.ts); combine逻辑在 observer 内部完成结构共享与replaceEqualDeep去重,保证引用稳定性(见 queriesObserver.ts)。
由于每个查询都有独立的QueryObserver,所以各个并行查询之间互不干扰:一个查询失败、重试或重新抓取,都不会影响其他查询的状态机。测试用例 packages/vue-query/src/tests/useQueries.test.ts 展示了"一个查询 error、另一个查询 success"的结果数组形态,正是这种隔离性的直接体现。
实践建议与常见误区
- 数量固定优先手动并行:固定数量时直接并排写
useQuery,代码可读性最好,且不引入额外的数组解构心智负担。 - 数量动态必须用
useQueries:查询列表来自运行时数据(如列表页勾选、用户列表详情)时,用queries选项接收 computed / ref / 函数形式,让 Vue 响应式驱动查询集合的增删。 - 善用
combine聚合状态:当 UI 需要"全部加载完成"或"存在失败"这类聚合信号时,用combine一次性推导,避免在模板中反复some/every。 - 警惕重复 queryKey:不要在
queries数组中重复使用相同 queryKey,否则数据会在查询之间共享,导致结果结构不符合预期。 - 注意
select类型回退:涉及内联select时,要么显式标注参数类型,要么统一用queryOptions定义后再传入useQueries,保持类型安全。 - 只在 setup 作用域内调用:开发模式下在
setup()或 effect scope 之外调用useQueries会收到内存泄漏警告,生产环境则不做检查——请务必在组合式函数或组件内调用。
总结
并行查询是 Vue Query 处理"同时发起多个独立请求"场景的标准方案:固定数量用useQuery并排声明,动态数量用useQueries响应式驱动。useQueries在 Vue 中拥有完整的响应式支持——ref 数组、getter queryKey、options 函数形式皆可响应,配合combine做结果聚合、queryOptions解决类型推断问题,再结合底层QueriesObserver的增量调度机制,足以支撑从仪表盘多接口聚合到列表详情批量加载等各类并发数据需求。
延伸阅读:Vue Query 概览 · useQuery 参考 · useQueries 参考 · queryOptions 参考
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考