TanStack Lit Query 的 Accessor 类型:让查询选项同时支持静态值与响应式 Getter
【免费下载链接】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/lit-query的核心类型别名Accessor<T>展开:它是 Lit Query 全部控制器 API 的"响应式入口",允许把查询/变更(mutation)选项以静态对象或零参 getter 两种形式传入。读完本文,你将理解Accessor的定义与readAccessor读取机制、它在控制器生命周期中"何时被求值"的底层实现,以及如何在 Lit 组件中正确编写静态 key 与依赖宿主状态的响应式 key。
1. Accessor 的定义:T | (() => T)
Accessor是 packages/lit-query/src/accessor.ts 中定义的类型别名:
type Accessor<T> = T | () => T;它的语义在源码注释中已经写得很明确(accessor.ts#L1-L12):
A value that can be passed directly or read from a zero-argument getter. Lit Query APIs read function accessors during host updates, so the getter can depend on reactive host state.
也就是说,Accessor<T>接受两种形式:
- 静态值:直接传
T本身,例如['todos']这样的查询 key 或完整选项对象; - 零参 getter:传一个
() => T函数,getter 在宿主(Lit 元素)更新时被重新求值,因此它可以闭包引用this.userId等响应式宿主状态。
类型参数只有一个T,表示"最终被读出来的值"的类型。文档给出的最小示例(docs/framework/lit/reference/type-aliases/Accessor.md):
const staticKey: Accessor<readonly unknown[]> = ['todos'] const reactiveKey: Accessor<readonly unknown[]> = () => ['todos', this.userId]两者类型完全兼容——这正是该类型别名存在的全部意义:让同一份 API 签名同时接纳"常量配置"与"随宿主状态变化的配置",而不需要拆成两个函数。
2. 读取机制:readAccessor
Accessor只是类型约定,真正的求值逻辑由同文件中的readAccessor函数完成(accessor.ts#L15-L17):
export function readAccessor<T>(value: Accessor<T>): T { return typeof value === 'function' ? (value as () => T)() : value }逻辑极其简洁:若传入的是函数则调用它取返回值,否则原样返回。整个 lit-query 包中,凡是"把Accessor解析为真实值"的地方都走这一个函数。从源码结构看,全部调用点分布在各个控制器与状态钩子中:
| 调用点 | 解析的对象 |
|---|---|
| createQueryController.ts#L302 | readAccessor(this.options),再交给client.defaultQueryOptions(...) |
| createMutationController.ts#L294 | readAccessor(this.options),再交给client.defaultMutationOptions(...) |
| createInfiniteQueryController.ts#L342 | 无限查询选项 |
| createQueriesController.ts#L296-L297 | 双层解析:readAccessor(optionsAccessor)后再readAccessor(resolvedOptions.queries) |
| useIsFetching.ts#L112、useIsMutating.ts#L112 | 过滤器条件readAccessor(this.filters) |
其中createQueriesController特别值得注意:它把queries数组本身也定义成了Accessor,因此在 types.ts#L74-L77 中,QueriesControllerOptions.queries的类型是Accessor<Array<...>>——即"查询列表"也可以随宿主状态整体变化。
3. 求值时机:静态选项与函数选项的两条路径
Accessor不是简单的"延迟取值",它在控制器内部对应两条明显不同的初始化路径。以 createQueryController.ts 为例:
构造函数中(createQueryController.ts#L109-L133):
constructor(host, options, queryClient?) { const initialClient = queryClient super(host, createPendingQueryResult(), queryClient) this.options = options if (!initialClient) { return } if (typeof options === 'function') { return // 函数 accessor:此刻不创建 observer } const defaulted = this.defaultOptions(initialClient) const observer = new QueryObserver(initialClient, defaulted) ... }可以推断出两条路径的行为差异:
- 静态选项:构造时立即
readAccessor求值、创建QueryObserver并计算乐观结果,挂载后马上订阅; - 函数选项:构造时直接返回,把求值推迟到宿主更新阶段——
QueryController.onHostUpdate()(createQueryController.ts#L153-L159):
protected onHostUpdate(): void { if (typeof this.options !== 'function') { return // 静态选项无需在每次更新时重读 } this.refreshOptions() }refreshOptions最终通过defaultOptions里的readAccessor(this.options)(createQueryController.ts#L302)重新执行 getter,得到与当前宿主状态一致的新选项,再通过observer.setOptions(options)应用到观测器上。
onHostUpdate的触发源头是 Lit 的 ReactiveController 生命周期:BaseController.hostUpdate() 在每次宿主更新时回调它;而结果变化后,setResult→queueUpdate(BaseController.ts#L159-L171)会用queueMicrotask触发host.requestUpdate(),从而完成"getter 读宿主状态 → 选项变化 → observer 更新 → 结果变化 → 宿主重渲染"的响应式闭环。createMutationController、createInfiniteQueryController等采用完全相同的模式(如 createMutationController.ts#L173-L179)。
这正是文档中那句 "Lit Query APIs read function accessors during host updates" 的具体含义:getter 的求值不是任意的,而是绑定在 Lit 的hostUpdate回调上,天然与响应式状态同步,且不会在非渲染时机重复求值。
4. 在控制器 API 中的实际形态
Accessor是 lit-query 对外 API 签名的通用包裹。types.ts 中导出的选项类型全部是"Accessor 包裹的选项":
// QueryControllerOptions = Accessor<CreateQueryOptions<...>> export type QueryControllerOptions<...> = Accessor< CreateQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> > // InfiniteQueryControllerOptions、MutationControllerOptions 同理 export type MutationControllerOptions<...> = Accessor< CreateMutationOptions<TData, TError, TVariables, TOnMutateResult> >因此所有create*Controller工厂函数的第二个参数都是Accessor<...>类型。index.ts#L5 已将Accessor与ValueAccessor一起从包入口导出,供组件作者自行标注 props 或内部工具类型。
一个依赖宿主状态的完整用法示意(userId为响应式 state 属性,getter 每次宿主更新时重读):
import { LitElement, html } from 'lit' import { createQueryController } from '@tanstack/lit-query' class UserTodosView extends LitElement { private userId = 1 private readonly todos = createQueryController(this, () => ({ queryKey: ['todos', this.userId], queryFn: () => fetch(`/api/users/${this.userId}/todos`).then((r) => r.json()), })) render() { const query = this.todos() if (query.isPending) return html`Loading...` if (query.isError) return html`Error` return html`<ul>${query.data.map((t) => html`<li>${t.title}</li>`)}</ul>` } }注意:当userId变化、宿主重新渲染时,onHostUpdate会用新选项重建 observer 的 options,查询随之切换。若选项与宿主状态无关,直接传静态对象(如仓库示例 examples/lit/basic/src/main.ts#L54-L60 中的{ queryKey: ['todos'], queryFn: fetchTodosFromServer })即可,构造期即完成初始化,省去更新期的重读开销。
5. 兄弟类型:ValueAccessor与createValueAccessor
读Accessor时应一并了解同文件定义的另一个类型ValueAccessor<T>(accessor.ts#L32-L43),两者方向相反、配合使用:
export type ValueAccessor<T> = (() => T) & { readonly current: T } export function createValueAccessor<T>(getter: () => T): ValueAccessor<T> { const accessor = (() => getter()) as ValueAccessor<T> Object.defineProperty(accessor, 'current', { get: getter, enumerable: true, }) return accessor }- 输入方向:
Accessor<T>——API 接受"值或 getter"; - 输出方向:
ValueAccessor<T>——控制器返回"可调用 + 带current属性"的对象,createValueAccessor通过Object.defineProperty让current成为一个实时 getter。
createQueryController的返回值QueryResultAccessor就是ValueAccessor<QueryObserverResult> & { refetch, suspense, destroy }(createQueryController.ts#L42-L51),所以渲染代码中this.todos()与this.todos.current等价。源码注释中的示例(accessor.ts#L26-L30):
const query = this.todos() const sameQuery = this.todos.current这样,Accessor系列类型在 lit-query 中构成了一个完整的对称设计:宿主状态经Accessor流入控制器,控制器结果经ValueAccessor流出到渲染代码。
6. 使用建议与小结
结合源码行为,使用Accessor时可以遵循几条经验:
- 与宿主状态无关的选项传静态对象。静态选项在构造期即完成 observer 初始化(createQueryController.ts#L128-L132),且跳过每次
hostUpdate中的重读分支; - 依赖 props/state 的选项传 getter。getter 会在宿主更新时被
readAccessor求值,闭包捕获的this.xxx总是最新值,无需手动重建控制器; - getter 应保持零参且轻量。它可能随宿主更新频繁执行,应避免在其中发起请求或产生副作用;求值结果会直接交给
defaultQueryOptions/defaultMutationOptions参与 observer 合并; queries列表、filters过滤器同样支持Accessor,例如useIsFetching(this, () => ({ queryKey: ['todos', this.userId] }))。
Accessor<T>看起来只是一行联合类型,但它是 Lit Query 将 Lit 响应式生命周期与 TanStack Query 观察者模型对接的关键抽象:一行T | (() => T),配合readAccessor与hostUpdate钩子,让"声明式配置"与"响应式配置"在同一组 API 签名下无缝共存。
【免费下载链接】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),仅供参考