TanStack Lit Query 中 IsFetchingAccessor 类型详解:useIsFetching 返回值的结构、用法与销毁机制
IsFetchingAccessor 是 TanStack Query 的 Lit 集成(@tanstack/lit-query)中 useIsFetching 工厂函数返回值的类型定义,位于 docs/framework/lit/reference/type-aliases/IsFetchingAccessor.md 对应的源码 packages/lit-query/src/useIsFetching.ts#L17。掌握它,你就理解了 Lit Query 中"计数类 API"的通用返回形态:一个既可调用、又可读取 .current 属性、还带有 destroy() 生命周期方法的响应式访问器。本文基于官方参考文档与仓库源码,完整拆解它的类型结构、使用方式、底层控制器实现与测试验证。
类型定义
官方参考文档给出的声明为:
type IsFetchingAccessor = ValueAccessor<number> & object;
仓库源码中的实际定义(packages/lit-query/src/useIsFetching.ts#L17):
export type IsFetchingAccessor = ValueAccessor<number> & { destroy: () => void }
两者语义一致:IsFetchingAccessor 是 ValueAccessor<number> 的交叉类型,并额外携带一个 destroy: () => void 成员。展开 ValueAccessor<number> 后,该类型等价于一个"可调用函数 + 只读 current 属性 + destroy 方法"的三元组合:
- 可调用:直接调用
accessor()即返回当前处于 fetching 状态、且匹配过滤条件的查询数量; current属性:以属性读取的方式获得同样的数值;destroy():手动销毁底层控制器,断开 QueryCache 订阅并把控制器从宿主组件上移除。
ValueAccessor 是 Lit Query 中多个 API 共享的公共类型,定义在 packages/lit-query/src/accessor.ts#L32-L34:
export type ValueAccessor<T> = (() => T) & {
readonly current: T
}
它的实现通过 createValueAccessor 完成(packages/lit-query/src/accessor.ts#L36-L43):先创建一个包裹 getter 的函数,再用 Object.defineProperty 挂上 current 只读 getter,二者读取的是同一个数据源,因此 accessor() 与 accessor.current 始终返回同值。这也是 IsFetchingAccessor 与 ValueAccessor<number> 交叉后仍声明为可调用对象的根本原因——函数本身就是访问器,current 只是同一个 getter 的别名。
useIsFetching 正是通过把 createValueAccessor 的结果与 destroy 方法合并来构造这个类型的实例(packages/lit-query/src/useIsFetching.ts#L147-L159):
export function useIsFetching(
host: ReactiveControllerHost,
filters: Accessor<QueryFilters> = {},
queryClient?: QueryClient,
): IsFetchingAccessor {
const controller = new IsFetchingController(host, filters, queryClient)
return Object.assign(
createValueAccessor(() => controller.current),
{
destroy: () => controller.destroy(),
},
)
}
可见 IsFetchingAccessor 并不是一个独立实现的类,而是"通用访问器工厂 + 控制器销毁句柄"的组合产物。该类型与 useIsFetching 均从包入口 packages/lit-query/src/index.ts#L43-L44 对外导出。
使用方式:调用函数或读取 current
参考文档说明:"Call the accessor or read its current property to get the number of currently fetching queries that match the filters."(调用该访问器,或读取其 current 属性,即可获取当前匹配过滤条件的正在 fetch 的查询数量。)
源码注释中的官方示例展示了在 Lit 组件中的标准用法(packages/lit-query/src/useIsFetching.ts#L131-L145):
import { LitElement, html } from 'lit'
import { useIsFetching } from '@tanstack/lit-query'
class TodosStatus extends LitElement {
private readonly todosFetching = useIsFetching(this, {
queryKey: ['todos'],
})
render() {
return html`<span>${this.todosFetching()} active todo fetches</span>`
}
}
三个参数依次为:
host: ReactiveControllerHost—— 拥有该缓存订阅的 Lit 响应式控制器宿主(通常是LitElement实例),控制器会调用host.addController(this)挂到宿主上,随宿主的连接/断开同步订阅与清理;filters: Accessor<QueryFilters>—— 查询过滤器,默认为空对象(匹配所有查询)。QueryFilters来自@tanstack/query-core,常用字段包括queryKey等;queryClient?: QueryClient—— 可选的显式 QueryClient。省略时,控制器会从最近的已连接QueryClientProvider通过 Lit Context 机制解析。
filters 的类型 Accessor<T> 同样定义在 packages/lit-query/src/accessor.ts#L13,是 T | (() => T) 的二元类型。这意味着过滤条件既可以是静态对象,也可以是一个 getter 函数。当传入函数时,Lit Query 会在每次宿主更新时重新读取它,使计数跟随宿主的响应式状态变化——这一点在下一节的控制器实现中有直接体现,并被仓库测试显式覆盖(详见"测试验证"小节)。
destroy() 成员:手动销毁控制器
参考文档中 IsFetchingAccessor 唯一的成员声明是:
destroy: () => void
从 useIsFetching 的实现可以看到,destroy 直接委托给内部 IsFetchingController 的 destroy(),最终执行的是基类 packages/lit-query/src/controllers/BaseController.ts#L104-L121 中的逻辑:
destroy(): void {
if (this.destroyed) {
return
}
this.destroyed = true
this.connected = false
this.connectionAttempt += 1
this.clearContextClient()
// ...
this.onDisconnected()
if ('removeController' in this.host) {
this.host.removeController(this)
}
}
其语义可以概括为四点:
- 幂等:重复调用不会重复清理(
destroyed标志位短路); - 断开状态:将控制器标记为已销毁并置为非连接状态,同时递增
connectionAttempt,使已排队的微任务回调因尝试序号不匹配而安全失效; - 清理上下文订阅:清除从
@lit/context获取 QueryClient 的订阅(clearContextClient()),随后触发onDisconnected(),在IsFetchingController中会执行this.unsubscribe?.()取消对QueryCache的订阅; - 脱离宿主:调用
host.removeController(this),使该控制器不再随宿主更新而被驱动。
因此,如果你手动持有访问器且希望立即释放其订阅(例如组件将被移除但尚未走完 Lit 生命周期,或你想在测试中精确控制清理时机),调用 todosFetching.destroy() 是明确的、安全的做法。
底层实现:IsFetchingController 如何得到这个数值
IsFetchingAccessor 的数值由私有类 IsFetchingController(packages/lit-query/src/useIsFetching.ts#L19-L114)产出,它继承 BaseController<number>,核心机制有四个:
1. 初始值为 0,按需计算。 构造函数中以 super(host, 0, queryClient) 将初始结果设为 0;若显式传入 queryClient,还会立即执行一次 computeValue() 写入结果。computeValue() 的实现非常简洁:
private computeValue(): number {
if (!this.queryClient) {
return 0
}
return this.queryClient.isFetching(readAccessor(this.filters))
}
即数值完全委托给 QueryClient.isFetching(filters),与 React/Svelte/Vue 各集成中 useIsFetching 的取值口径一致;readAccessor 负责兼容"静态对象或 getter"两种过滤器形态。
2. 订阅 QueryCache,实时刷新计数。 控制器在 onConnected() 时调用 subscribe():
private subscribe(): void {
if (!this.queryClient) {
return
}
if (this.unsubscribe) {
return
}
this.unsubscribe = this.queryClient.getQueryCache().subscribe(() => {
this.setResult(this.computeValue())
})
}
订阅的是整个 QueryCache 的变更事件(不区分具体查询),任何查询进入/退出 fetching 状态都会触发一次重算并 setResult。由于 Lit 集成的设计是"控制器不直接触发渲染,而是请求宿主更新",BaseController.setResult(packages/lit-query/src/controllers/BaseController.ts#L136-L145)在数值变化时会通过 queueMicrotask 调 host.requestUpdate() 排入一次宿主更新,保证渲染读取 accessor() 时拿到的是最新值。
3. 响应式过滤器在宿主更新时重读。 onHostUpdate() 中:
protected onHostUpdate(): void {
if (typeof this.filters !== 'function') {
return
}
this.setResult(this.syncClient() ? this.computeValue() : 0)
}
只有当 filters 是函数时才在宿主更新路径上重算——因为 getter 可能依赖响应式的宿主状态,需要在每次更新时重新读取;静态对象过滤器则不需要,避免无谓计算。
4. QueryClient 的动态解析。 省略 queryClient 参数时,BaseController 通过派发 ContextEvent(packages/lit-query/src/controllers/BaseController.ts#L208-L251)向祖先 QueryClientProvider 请求 client,并维护 pre-connect / awaiting-context / bound / missing 四种解析状态;client 缺失或切换时会重新订阅(onQueryClientChanged),并在缺失时把计数回落到 0。另外,current 属性在解析状态为 missing 时会抛出 createMissingQueryClientError()(packages/lit-query/src/controllers/BaseController.ts#L147-L153),这是读取访问器时可能遇到的边界行为。
测试验证
仓库测试 packages/lit-query/src/tests/counters-and-state.test.ts 中对 IsFetchingAccessor 的行为有直接覆盖:
- 用例
S1: useIsFetching tracks filters and filter reactivity(约 L360-L420)验证了静态过滤器与响应式过滤器两条路径:对全部查询创建isFetchingAll(传{}),对() => activeFilter创建isFetchingFiltered。两个查询并发时isFetchingAll() === 2、isFetchingFiltered() === 1;随后把activeFilter切到另一个 key 并触发host.update(),计数仍保持1(因为新过滤的 key 也在 fetch 中),直到两个查询分别 resolve 后计数归零。这正好印证了前述第 3 点:getter 形式的过滤器随宿主更新重读。 - 同一文件中还有组合用例(约 L337-L352):
useIsFetching与useIsMutating、useMutationState并用,查询进入 fetch 时isFetching() === 1,成功后回到0,验证了计数随QueryCache订阅实时收敛。 - 控制器宿主侧的生命周期(connect/disconnect/destroy 与 client 切换下的重订阅)则由 packages/lit-query/src/tests/base-controller.test.ts 与 packages/lit-query/src/tests/client-switch-controllers.test.ts 覆盖。
小结与参考
IsFetchingAccessor = ValueAccessor<number> & { destroy: () => void },是 Lit Query 中把"QueryCache 上的计数统计"暴露给 Lit 组件的统一接口:
- 取值:
count()或count.current,两者同值; - 过滤:
filters支持QueryFilters对象或返回它的 getter,getter 形态可随宿主状态响应式更新; - 生命周期:订阅 QueryCache,client 缺失时计数为
0;destroy()幂等地取消订阅并removeController; - 同源 API:
useIsMutating返回的IsMutatingAccessor、useMutationState返回的MutationStateAccessor采用同样的"ValueAccessor + destroy"形态,可对照 packages/lit-query/src/useIsMutating.ts 与 packages/lit-query/src/useMutationState.ts 阅读。
延伸阅读可参考仓库内 Lit 集成文档 docs/framework/lit/quick-start.md、docs/framework/lit/overview.md 以及 packages/lit-query/README.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00