首页
/ Lit Query 并行查询(Parallel Queries)实战指南:从手动多查询到动态合并

Lit Query 并行查询(Parallel Queries)实战指南:从手动多查询到动态合并

2026-09-07 20:40:50作者:柏廷章Berta

TanStack Query 的 Lit 适配层 @tanstack/lit-query 提供了一组以 Lit ReactiveController 为核心的查询控制器,其中**并行查询(Parallel Queries)**用于解决"多个互相独立的请求同时发起、UI 不必等待某个请求先完成再发下一个"的问题。本文围绕 parallel-queries.md 指南,讲解固定数量查询的手动并行写法、随宿主状态变化的动态并行写法,以及用 combine 将结果数组归一为单一派生值的技巧,并结合本仓库 lit-query 包源码解释其底层订阅与触发更新机制。

什么是并行查询

并行查询指在同一时刻并行运行的多个查询。与"串行/依赖查询"(前一个请求的结果作为下一个请求的参数)相对,并行查询之间没有数据依赖,因此可以同时发起,缩短首屏总等待时间。

@tanstack/lit-query 中,并行能力来自 Lit 的控制器(Controller)生命周期:每个查询控制器都会在宿主元素(Host)连接(connected)时通过 hostConnectedonConnected 注册订阅,多个控制器挂在同一个 Host 上即天然并行、互不阻塞。根据查询数量是否固定,官方指南给出了两种模式:

  • 手动并行查询(Manual Parallel Queries):查询数量固定时,在同一宿主上创建多个 createQueryController
  • 动态并行查询(Dynamic Parallel Queries):查询数量随宿主状态变化时,使用 createQueriesController 维护一个查询数组。

手动并行查询:数量固定时创建多个控制器

当查询数量在编译期就可确定时,推荐的做法是在同一个 LitElement 宿主上创建多个查询控制器,例如一个仪表盘同时加载用户、团队与项目三类数据:

import { LitElement, html } from 'lit'
import { createQueryController } from '@tanstack/lit-query'

class DashboardView extends LitElement {
  private readonly users = createQueryController(this, {
    queryKey: ['users'],
    queryFn: fetchUsers,
  })

  private readonly teams = createQueryController(this, {
    queryKey: ['teams'],
    queryFn: fetchTeams,
  })

  private readonly projects = createQueryController(this, {
    queryKey: ['projects'],
    queryFn: fetchProjects,
  })

  render() {
    const users = this.users()
    const teams = this.teams()
    const projects = this.projects()

    if (users.isPending || teams.isPending || projects.isPending) {
      return html`Loading...`
    }

    if (users.isError || teams.isError || projects.isError) {
      return html`Unable to load dashboard`
    }

    return html`
      <dashboard-summary
        .users=${users.data}
        .teams=${teams.data}
        .projects=${projects.data}
      ></dashboard-summary>
    `
  }
}

关键行为与实现依据

createQueryController.ts 源码可以看出该模式的几个要点:

  • 共享宿主、并行订阅createQueryController 的第一个参数是 ReactiveControllerHost,创建时会执行 host.addController(this)(见 BaseController.ts)。三个控制器挂到同一个 DashboardView 上,当宿主连接时各自走 onConnected():先 syncClient() 解析 QueryClient,再 refreshOptions() 应用默认选项,最后 subscribe() 把观察者订阅到 QueryObserver。三个观察者彼此独立,fetch 请求并行发出,谁先完成谁先触发 host.requestUpdate()
  • 访问器(Accessor)语义:返回对象是可调用函数,同时也暴露 currentrefetchsuspensedestroy(见 createQueryController.ts)。文档中的 this.users() 等价于读取最新一次 QueryObserverResult
  • 初次渲染的安全结果:尚未拿到数据的阶段,控制器内置一个 pending 占位结果(createPendingQueryResultstatus: 'pending'isPending: true),这正是 users.isPending 分支可用的前提,见 createQueryController.ts
  • QueryClient 解析规则:文档强调"如果未显式传入 QueryClient,每个控制器都会解析最近连接的 QueryClientProvider"。机制上,控制器会通过 dispatchContextRequest 派发 ContextEvent(queryClientContext, ...) 向上查找 Provider(见 BaseController.tscontext.ts),因此并行查询的多个控制器默认共享同一份缓存。也可以在第三个参数显式传入 QueryClient(见 createQueryController.ts 的类型说明)。

动态并行查询:使用 createQueriesController

查询的数量会随宿主状态变化时(例如根据一组用户 id 渲染每个用户的详情),逐个声明控制器不再可行,应改用 createQueriesController。它接受一个 queries 数组,并返回一个访问器,读取结果为查询结果数组

当查询列表依赖响应式宿主字段时,把 options 写成**getter(options getter)**即可让列表跟随宿主状态刷新:

import { LitElement, html } from 'lit'
import { createQueriesController } from '@tanstack/lit-query'

class UsersDetails extends LitElement {
  static properties = {
    userIds: { attribute: false },
  }

  userIds: Array<string> = []

  private readonly users = createQueriesController(this, () => ({
    queries: this.userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUserById(id),
    })),
  }))

  render() {
    const userQueries = this.users()

    return html`
      <ul>
        ${userQueries.map((query, index) => {
          if (query.isPending) return html`<li>Loading...</li>`
          if (query.isError) return html`<li>Error loading user</li>`

          return html`<li>${this.userIds[index]}: ${query.data.name}</li>`
        })}
      </ul>
    `
  }
}

注意几个要点:

  • 结果顺序与输入顺序一致createQueriesController 会按 queries 数组的元素位置映射每个查询结果,因此渲染时可以放心用 userQueries[index]userIds[index] 一一对应,无需额外排序。
  • 单个元素的渲染模式与单查询控制器完全一致:读取 query.isPendingquery.isErrorquery.data 等字段。

底层实现细节

createQueriesController.tsBaseController.ts 可以还原它的运行机制:

  • getter 触发刷新:控制器会在构造时判断"options 本身是函数,或 options.queries 是函数"(shouldRefreshOnHostUpdate,见 createQueriesController.ts)。由于示例中传入的是函数,每次宿主更新(hostUpdate)都会走 onHostUpdate() 重新调用 getter、执行 refreshOptions(),再通过 observer.setQueries(...) 把新的查询集合推给底层的 QueriesObserver
  • 多个查询共享一个观察者:动态模式下不是"每个查询一个 QueryObserver",而是由一个来自 @tanstack/query-coreQueriesObserver 统一协调多个子查询,控制器收到其订阅回调后更新结果(见 createQueriesController.ts)。createQueriesController 的完整函数签名可查阅 createQueriesController.md
  • 默认选项统一应用:每个子查询在进入观察者前都会经过 client.defaultQueryOptions(...) 补全全局默认值,并设置 _optimisticResults: 'optimistic' 以支持乐观结果(见 createQueriesController.ts)。
  • 类型安全的元组推导:源码中 CreateQueriesResults/GetCreateQueriesResult 等类型会对 queries 数组逐元素做查询输入与结果的一一映射(见 createQueriesController.ts),这正是示例里 query.data.name 能拿到精确类型的原因。

用 combine 合并多个查询结果

当组件希望拿到的不是查询结果数组,而是由它们派生出的单一值(例如一个仪表盘模型)时,可使用 combine 选项。combine 接收查询结果数组,返回任意派生值,控制器会把该派生值作为访问器的返回值:

private readonly dashboard = createQueriesController(this, {
  queries: [
    { queryKey: ['stats'], queryFn: fetchStats },
    { queryKey: ['projects'], queryFn: fetchProjects },
  ],
  combine: ([stats, projects]) => ({
    activeUsers: stats.data?.activeUsers ?? 0,
    projects: projects.data ?? [],
    isPending: stats.isPending || projects.isPending,
    isError: stats.isError || projects.isError,
  }),
})
render() {
  const dashboard = this.dashboard()

  if (dashboard.isPending) return html`Loading...`
  if (dashboard.isError) return html`Unable to load dashboard`

  return html`
    <p>Total projects: ${dashboard.projects.length}</p>
    <p>Active users: ${dashboard.activeUsers}</p>
  `
}

从源码看,combine 的执行包含一次关键的引用稳定化:控制器用 replaceEqualDeep 对比上一次的合并结果,若两次 combine 产出结构相等,则继续复用旧引用、不再触发宿主更新(见 createQueriesController.ts)。这意味着只要派生对象的内容没有实质变化,就不会引起额外的 requestUpdate 与重复渲染,这在仪表盘这类高频刷新场景中尤为重要。

无 QueryClient 时的占位 combine

源码还处理了一个边界:当 Provider 尚未连接、客户端不可用且 combine 提前被读取时,控制器会先用占位(placeholder)查询结果构造并应用一次 combinecreatePlaceholderResult,见 createQueriesController.ts)。如果某个子查询带有 initialData,占位结果会优先物化该初始数据,使 combine 在首帧就能拿到真实数据形状(相关用例见 queries-controller.test.ts 中 "placeholder combine materializes defined initialData" 等测试)。

注意事项:重复的 queryKey 会共享缓存

官方指南给出了一条明确的警告:queries 数组中若出现同一个 queryKey 多次,这些条目会共享同一份缓存数据。因为 TanStack Query 以 [queryKey, queryClient] 作为缓存与观察的标识,两个相同 key 的查询会命中同一个 cache entry。如果每行都需要相互独立的查询状态(例如同一批数据的多行分别展示各自的加载/错误状态,或参数即将变化前做差异化处理),应先对重复的 key 去重后再生成查询集合,例如先对 userIds 去重:

const ids = [...new Set(this.userIds)]
queries: ids.map((id) => ({
  queryKey: ['user', id],
  queryFn: () => fetchUserById(id),
}))

与此相关的最佳实践还有:使用可序列化、结构清晰的查询键(见 query-keys.md),以及在需要加载更多等"分页式并行"场景参考 infinite-queries.md

小结:三种并行形态的选型对照

场景 推荐 API 结果形态 结果顺序
查询数量固定(如仪表盘 3 块数据) 多个 createQueryController 各自独立的 QueryObserverResult 不适用(按需取名)
数量随状态变化(如按 id 列表渲染详情) createQueriesController + options getter QueryObserverResult[] queries 输入一一对应
需要把多个结果合成一个派生值 createQueriesController + combine 任意自定义对象 在 combine 解构时保持输入顺序

三种形态共享同一套原则:控制器挂载在 Lit 宿主上并随生命周期注册/销毁订阅(subscribe/unsubscribeObserver,见 BaseController.ts),未显式传 QueryClient 时统一从最近的 QueryClientProvider 解析客户端,从而保证并行查询天然复用同一缓存与全局默认配置。

若想验证上述行为或深入了解状态机细节,可直接阅读本仓库的单元测试 queries-controller.test.tsquery-controller.test.ts,其中覆盖了并行合并、动态增删查询、combine 稳定引用以及 Provider 切换等多类用例。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388