☰
TanStack Query QueryObserver 深入解析:观察者模式、订阅生命周期与 useQuery 底层实现
2026/10/14 19:52:14 网站建设 项目流程

TanStack Query QueryObserver 深入解析:观察者模式、订阅生命周期与 useQuery 底层实现

【免费下载链接】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

QueryObserver是 TanStack Query 中负责"观察并切换查询"的核心类,它把查询缓存中的底层状态包装成组件友好的结果对象,并通过订阅机制驱动 UI 更新。无论你用的是 React Query、Solid Query、Svelte Query 还是 Vue Query,所有useQuery类 API 最终都建立在QueryObserver之上。读完本文,你将掌握QueryObserver的构造与订阅用法、它与useQuery的等价选项关系、订阅生命周期内部机制、结果派生与性能优化手段,以及它如何通过属性跟踪和notifyOnChangeProps减少无谓渲染。

QueryObserver 是什么

按照 docs/reference/QueryObserver.md 的定义:QueryObserver可以用来观察(observe)查询,并在查询之间切换(switch)。

它是 TanStack Query 架构中"查询缓存(QueryCache)"与"视图层(UI)"之间的桥梁。一个QueryObserver实例持有对QueryClient的引用和一个QueryObserverOptions配置,通过订阅机制监听查询状态变化,再把变化整理成QueryObserverResult结果对象分发给所有监听者。

从源码结构看,QueryObserver继承自Subscribable<TListener>(见 subscribable.ts),后者用Set<TListener>维护监听器列表,并提供三个关键能力:

  • subscribe(listener):注册监听器,返回一个用于退订的unsubscribe函数;
  • hasListeners():判断当前是否还有监听者;
  • 受保护的onSubscribe()/onUnsubscribe()钩子:在首个订阅者加入或最后一个订阅者离开时触发,由QueryObserver覆写以驱动真实的获取逻辑。

QueryObserver覆写了这两个钩子:onSubscribe()在第一个监听者加入时把自身注册到当前查询(#currentQuery.addObserver(this)),并在需要时触发首次请求(shouldFetchOnMount);onUnsubscribe()在没有监听者时调用destroy()清理定时器并从查询上移除自己。这就是"订阅即获取、退订即清理"的生命周期基础。

快速上手:直接实例化与订阅

QueryObserver可以直接脱离任何框架使用,只需传入一个QueryClient和查询选项。原文档给出的最小示例(见 docs/reference/QueryObserver.md):

const observer = new QueryObserver(queryClient, { queryKey: ['posts'] }) const unsubscribe = observer.subscribe((result) => { console.log(result) unsubscribe() })

这里queryClient通常来自new QueryClient();subscribe的回调会在每次查询结果变化时收到一个QueryObserverResult对象,且subscribe立即返回退订函数。上面的例子在收到第一次通知后就主动退订,适合一次性观察。

一个更完整的可用示例(结合 queryObserver.ts 的构造函数签名):

import { QueryClient, QueryObserver } from '@tanstack/query-core' const queryClient = new QueryClient() const observer = new QueryObserver(queryClient, { queryKey: ['posts'], queryFn: () => fetch('/api/posts').then((res) => res.json()), staleTime: 5_000, }) const unsubscribe = observer.subscribe((result) => { console.log(result.status, result.data, result.isFetching) }) // 观察结束或组件卸载时退订 unsubscribe()

构造QueryObserver时(queryObserver.ts),构造函数会做三件事:

  1. 保存client引用;
  2. 调用bindMethods()绑定refetch方法,保证它被取出后this依然指向 observer;
  3. 调用setOptions(options)完成选项默认化、查询实例绑定和结果初始化。

也就是说,仅仅new QueryObserver(...)还不会发起请求。真正触发请求的时机有两个:一是调用subscribe且成为第一个订阅者(onSubscribe中执行shouldFetchOnMount判断),二是显式调用refetch()或fetch()。测试 queryObserver.test.tsx 中'should trigger a fetch when subscribed'验证了这一点:订阅后queryFn恰好被调用一次。

Options:与 useQuery 完全相同的配置

原文档明确指出:QueryObserver的 Options 与useQuery完全一致(原文档链接指向 useQuery)。

这并非巧合,而是设计使然:useQuery在 React 侧只是一个薄封装,它把用户传入的 options 原样交给QueryObserver。见 useQuery.ts:

return useBaseQuery(options, QueryObserver, queryClient)

而 useBaseQuery.ts 中正是用这些 options 实例化 observer:

const [observer] = React.useState( () => new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>( client, defaultedOptions, ), )

因此,useQuery 文档中列出的所有选项——queryKey、queryFn、enabled、staleTime、gcTime、refetchInterval、refetchOnWindowFocus、refetchOnReconnect、refetchOnMount、retry、retryOnMount、placeholderData、initialData、select、structuralSharing、notifyOnChangeProps、throwOnError、suspense、_optimisticResults等——都适用于QueryObserver。核心选项可归纳为以下几类:

类别选项作用
数据定位queryKey、queryHash定位/标识缓存中的查询
数据获取queryFn返回 Promise 的请求函数
开关控制enabled布尔值或返回布尔值的回调,控制是否启用查询
新鲜度staleTime数据被视为"新鲜"的时长,影响自动重取与 stale 判定
缓存gcTime无订阅者后缓存留存时间
重取策略refetchOnMount、refetchOnWindowFocus、refetchOnReconnect、refetchInterval、refetchIntervalInBackground各种场景下的自动重取规则
错误处理retry、retryDelay、retryOnMount、throwOnError重试与错误抛出策略
数据变换select、structuralSharing派生数据与结构共享
初始/占位initialData、placeholderData无真实数据时展示的数据
渲染优化notifyOnChangeProps、trackedProps控制通知与属性跟踪
框架集成suspense、_optimisticResultsReact Suspense 与乐观结果

这些选项在setOptions中会被queryClient.defaultQueryOptions(options)处理为"默认化"(defaulted)配置,并经过合法性校验——例如enabled必须是布尔值或返回布尔值的回调,否则直接抛出'Expected enabled to be a boolean or a callback that returns a boolean'(queryObserver.ts)。

订阅生命周期:从 onSubscribe 到 destroy

QueryObserver的生命周期由订阅驱动,其核心脉络记录在 queryObserver.ts 中,可用下图概括:

new QueryObserver(client, options) │ ▼ setOptions(options) ──► defaultQueryOptions / updateQuery / updateResult │ ▼ subscribe(listener) ──► Subscribable.subscribe │ │ │ listeners.size === 1 ? │ │ │ ┌────────┴────────┐ │ ▼ ▼ │ currentQuery.addObserver(this) │ shouldFetchOnMount ? executeFetch : updateResult │ updateTimers() ▼ query 更新 ──► onQueryUpdate() ──► updateResult() + updateTimers() │ ▼ unsubscribe() ──► 无监听者 ? destroy() : (保留,继续观察) │ ▼ destroy() ──► 清空 listeners / clear stale & refetch timers / removeObserver

各阶段的关键行为:

  • 首次订阅(onSubscribe,queryObserver.ts):仅当listeners.size === 1(即第一个订阅者)时,把 observer 注册到当前查询;随后根据shouldFetchOnMount决定是立即#executeFetch()还是仅updateResult(),最后调用#updateTimers()启动 stale 超时与refetchInterval定时器。
  • 选项更新(setOptions,queryObserver.ts):重新默认化选项、必要时切换查询实例(#updateQuery)、向 QueryCache 发送observerOptionsUpdated通知,并在满足shouldFetchOptionally时触发可选重取,最后重建结果与定时器。测试'should notify cache listeners when setOptions is called'(queryObserver.test.tsx)验证了选项变更会通知缓存监听者。
  • 查询状态更新(onQueryUpdate,queryObserver.ts):查询数据变化时由 Query 回调触发,先updateResult()通知监听者,再同步更新定时器。
  • 退订与销毁(onUnsubscribe→destroy,queryObserver.ts):当最后一个监听者离开时,清空listeners、清除 stale 超时与重取间隔(借助timeoutManager),并调用#currentQuery.removeObserver(this)从查询上摘除自己。测试'should stop retry when unsubscribing'(queryObserver.test.tsx)表明退订还会终止进行中的重试。

值得注意的实现细节:stale 超时使用了timeoutManager.setTimeout,并且源码注释说明"超时有时会在 stale 过期前 1ms 触发,因此统一加 1ms 补偿"(queryObserver.ts);refetchInterval定时器则在触发前检查refetchIntervalInBackground || focusManager.isFocused(),只有页面处于焦点时才在后台重取(queryObserver.ts)。

在查询之间切换

QueryObserver的另一个核心职责是"在查询之间切换"。这通过#updateQuery()(queryObserver.ts)实现:

#updateQuery(): void { const query = this.#client.getQueryCache().build(this.#client, this.options) if (query === this.#currentQuery) { return } const prevQuery = this.#currentQuery this.#currentQuery = query this.#currentQueryInitialState = query.state if (this.hasListeners()) { prevQuery?.removeObserver(this) query.addObserver(this) } }

切换逻辑非常明确:用新 options 通过QueryCache.build解析出(或复用缓存中已有的)目标 Query;若与当前查询不同,则记录新查询,并在有监听者时从旧查询移除自己、加入新查询。也就是说,同一个 observer 实例可以持续观察多个 queryKey——当setOptions传入新的queryKey时,观察目标自动切换。

测试'should notify when switching query'(queryObserver.test.tsx)验证了切换时监听者会收到新查询的结果通知。此外,observer 内部维护#lastQueryWithDefinedData记录最近一次"有数据"的查询,用于在切换后为placeholderData函数提供上一份数据(queryObserver.ts),这正是分页/详情页切换时"保持旧数据可见"能力(keepPreviousData)的底层来源。

结果派生:createResult 如何组装 QueryObserverResult

createResult(queryObserver.ts)是QueryObserver最核心的纯计算函数:它接收一个 Query 实例和 options,产出一个完整的QueryObserverResult。其派生过程按顺序包含:

  1. 基础状态:从query.state复制status(pending/success/error)、fetchStatus(idle/fetching/paused)、data、error等;
  2. 乐观结果:当options._optimisticResults存在时,根据shouldFetchOnMount/shouldFetchOptionally提前把状态置为fetching,若为isRestoring则强制fetchStatus = 'idle';
  3. placeholderData 处理:在data === undefined && status === 'pending'时,若配置了placeholderData,则计算占位数据并把status提升为success、标记isPlaceholderData = true;placeholderData可以是值或函数,函数会收到"最近一次有数据查询"的data与query参数(queryObserver.ts);
  4. select 派生:若配置了select,对(含占位)数据执行选择器;结果通过replaceData(内部依赖structuralSharing做结构共享)和#selectResult缓存做记忆化,数据与选择器未变时不会重复执行(对应测试'should not run the selector again if the data and selector did not change',queryObserver.test.tsx);select抛错会被捕获进#selectError并转化为status: 'error',且切换查询后会清空避免错误泄漏;
  5. 派生布尔量:最终结果包含isPending、isSuccess、isError、isLoading、isInitialLoading、isFetching、isRefetching、isPaused、isPlaceholderData、isFetched、isFetchedAfterMount、isLoadingError、isRefetchError、isStale、isEnabled以及refetch方法、dataUpdatedAt、errorUpdatedAt、failureCount、failureReason等完整字段。

isLoading = isPending && isFetching这一派生关系值得特别说明:当查询被enabled: false禁用时,fetchStatus为idle,因此isLoading为false,这正是官方推荐"禁用场景下用isLoading而非isPending"的原因(见 useQuery 中的依赖查询示例)。

staleTime特殊值'static'也在源码中有对应逻辑:shouldFetchOn中,当staleTime为'static'时直接返回false,即静态数据永远不会被自动重取(queryObserver.ts)。

属性跟踪与通知优化:notifyOnChangeProps 与 trackResult

QueryObserver内置了一套渲染优化机制,避免"任何字段变化都触发所有监听者":

  • 属性跟踪:trackResult用Proxy包装结果对象,读取任何属性时都会记录到#trackedProps(queryObserver.ts)。在 React 中,useBaseQuery在没有notifyOnChangeProps时返回observer.trackResult(result)(useBaseQuery.ts),因此只有组件实际读取过的属性才会计入跟踪集合。
  • 按需通知:updateResult中的shouldNotifyListeners()(queryObserver.ts)决定是否真的调用监听者:notifyOnChangeProps为'all'时全量通知;为函数时取其返回值作为允许通知的属性集合;否则使用#trackedProps。只有"当前结果中确实变化了的属性"命中了允许集合,才会触发通知。测试'should notify listeners when notifyOnChangeProps is a function returning props that changed'与'should not notify listeners when notifyOnChangeProps is a function returning props that did not change'(queryObserver.test.tsx)正反两面验证了该行为。
  • 批量通知:所有监听者回调与 QueryCache 的observerResultsUpdated通知被包裹在notifyManager.batch()中,实现一次渲染周期内的合并通知(queryObserver.ts)。
  • 浅比较短路:updateResult在shallowEqualObjects(nextResult, prevResult)为真时直接返回,不做任何通知(queryObserver.ts)。

手动获取:refetch、fetch 与 fetchOptimistic

除了订阅驱动,QueryObserver还提供命令式获取能力:

  • refetch(options?)(queryObserver.ts):返回Promise<QueryObserverResult>,是结果对象上refetch字段的实现。cancelRefetch默认在源码fetch路径中被置为true(queryObserver.ts),即重取会取消进行中的旧请求。
  • fetch(options?):受保护的内部方法,调用#executeFetch后立即updateResult()并返回最新结果。
  • fetchOptimistic(options)(queryObserver.ts):为 Suspense 场景设计——先在缓存中build出查询并发起query.fetch(),同时订阅 QueryCache 等待数据落库,最后用Promise.race取两者中先完成的结果。React 侧的useSuspenseQuery正是通过fetchOptimistic在 Suspense 抛出前预先获取数据(suspense.ts)。
  • getOptimisticResult(options)(queryObserver.ts):在不订阅的情况下用给定 options 模拟计算结果,供 React 渲染期提前获得"乐观"状态。useBaseQuery在useSyncExternalStore之前就调用它来获取渲染结果(useBaseQuery.ts),随后才订阅。

useBaseQuery中还有一处细节印证了updateResult的必要性:订阅建立后立即调用一次observer.updateResult(),以"补上创建 observer 与订阅之间可能错过的查询更新"(useBaseQuery.ts)。

禁用与定时器的协同

QueryObserver对enabled的处理贯穿整个实现:

  • 禁用(enabled: false)时,shouldLoadOnMount/shouldFetchOn/shouldFetchOptionally/isStale均短路返回false,因此不会自动请求、不会判定为 stale;
  • #shouldScheduleTimer要求enabled !== false且非服务端环境才允许调度定时器(queryObserver.ts);
  • 测试'should not schedule timers for disabled observers'(queryObserver.test.tsx)确认禁用 observer 不启动任何定时器。

注意enabled支持回调形式(如() => enabled),每次判定时通过resolveQueryValue求值,使选项在函数上下文中也能动态计算。测试中'enabled is a callback that initially returns false'组(queryObserver.test.tsx)验证了回调返回false时订阅也不会发起请求。

多订阅者与框架集成

QueryObserver天然支持多监听者:Subscribable.listeners是Set,updateResult会遍历所有监听者分发同一份#currentResult。测试'should be able to handle multiple subscribers'(queryObserver.test.tsx)验证了这一点。多个组件订阅同一个 queryKey 时,它们共享缓存中的同一个 Query 实例,但各自持有独立的QueryObserver,互不干扰。

各框架层的集成路径清晰一致:

  • React:useQuery→useBaseQuery(options, QueryObserver, ...)(useQuery.ts);
  • React 多查询:useQueries在内部直接new QueryObserver(client, opts)管理一组观察者(useQueries.ts);
  • 聚合观察:QueriesObserver(queriesObserver.ts)同样是基于QueryObserver的组合器,用于useQueries的框架无关实现;
  • 其他框架:Solid Query、Svelte Query、Vue Query 以及 Lit 的createQueryController(lit-query/src/createQueryController.ts)均以QueryObserver为底层,只是订阅到各自响应式系统的桥接方式不同。

正因如此,QueryObserver的选项、生命周期与结果结构在所有框架中保持一致——这也解释了为什么文档直接说"Options 与useQuery完全相同"。

小结

QueryObserver是 TanStack Query 中承上启下的核心构件:

  • 观察与切换:一个实例可随 options 变化在多个查询间切换,订阅驱动获取与清理(见 queryObserver.ts);
  • 选项统一:Options 与 useQuery 完全一致,框架层只是薄封装;
  • 结果派生:createResult集中处理 placeholderData、select、结构共享与全部派生布尔量;
  • 性能优化:trackResult属性跟踪 +notifyOnChangeProps按需通知 +notifyManager.batch批量通知,让监听者只在真正关心的字段变化时被唤醒;
  • 可测试性:queryObserver.test.tsx 用近 2000 行测试覆盖了订阅触发、查询切换、选择器记忆化、禁用定时器、退订停止重试、后台重取开关等全部关键路径,是理解其行为边界的最佳参考。

无论是排查useQuery的重取行为,还是需要脱离框架在 Node/脚本环境中监听查询状态,QueryObserver都是值得直接使用的底层 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),仅供参考

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

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

立即咨询