TanStack Lit Query 的 Accessor 类型:让查询选项同时支持静态值与响应式 Getter
2026/9/8 21:24:55 网站建设 项目流程

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#L302readAccessor(this.options),再交给client.defaultQueryOptions(...)
createMutationController.ts#L294readAccessor(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) ... }

可以推断出两条路径的行为差异:

  1. 静态选项:构造时立即readAccessor求值、创建QueryObserver并计算乐观结果,挂载后马上订阅;
  2. 函数选项:构造时直接返回,把求值推迟到宿主更新阶段——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() 在每次宿主更新时回调它;而结果变化后,setResultqueueUpdate(BaseController.ts#L159-L171)会用queueMicrotask触发host.requestUpdate(),从而完成"getter 读宿主状态 → 选项变化 → observer 更新 → 结果变化 → 宿主重渲染"的响应式闭环。createMutationControllercreateInfiniteQueryController等采用完全相同的模式(如 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 已将AccessorValueAccessor一起从包入口导出,供组件作者自行标注 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. 兄弟类型:ValueAccessorcreateValueAccessor

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.definePropertycurrent成为一个实时 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时可以遵循几条经验:

  1. 与宿主状态无关的选项传静态对象。静态选项在构造期即完成 observer 初始化(createQueryController.ts#L128-L132),且跳过每次hostUpdate中的重读分支;
  2. 依赖 props/state 的选项传 getter。getter 会在宿主更新时被readAccessor求值,闭包捕获的this.xxx总是最新值,无需手动重建控制器;
  3. getter 应保持零参且轻量。它可能随宿主更新频繁执行,应避免在其中发起请求或产生副作用;求值结果会直接交给defaultQueryOptions/defaultMutationOptions参与 observer 合并;
  4. queries列表、filters过滤器同样支持Accessor,例如useIsFetching(this, () => ({ queryKey: ['todos', this.userId] }))

Accessor<T>看起来只是一行联合类型,但它是 Lit Query 将 Lit 响应式生命周期与 TanStack Query 观察者模型对接的关键抽象:一行T | (() => T),配合readAccessorhostUpdate钩子,让"声明式配置"与"响应式配置"在同一组 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询