TanStack Query(Preact Query)缓存机制实战:从挂载、后台刷新到垃圾回收的完整生命周期

发布时间:2026/9/9 13:59:46
TanStack Query(Preact Query)缓存机制实战:从挂载、后台刷新到垃圾回收的完整生命周期 TanStack QueryPreact Query缓存机制实战从挂载、后台刷新到垃圾回收的完整生命周期【免费下载链接】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/queryTanStack Query 的缓存层是其异步状态管理的核心数据以queryKey为索引存放于内存缓存中配合staleTime数据新鲜期与gcTime垃圾回收期两大时间配置驱动读缓存秒开、后台静默刷新、过期自动清理的完整生命周期。本文以tanstack/preact-query的useQuery为切入点完整复现官方缓存示例对应指南 docs/framework/preact/guides/caching.md正文内容与 React 版一致并结合本仓库源码拆解每一次挂载、卸载与重新挂载背后 Query 实例、Observer 与缓存条目之间的真实互动帮助你在 Preact 应用中理解并驾驭这套缓存模型。阅读本指南之前建议先通读 Important Defaults重要默认配置其中解释了 TanStack Query 的激进但合理的默认行为本文中的许多现象正是这些默认值叠加的产物。示例前提两个决定缓存行为的关键时间缓存示例的故事基于两个默认配置展开它们是理解整个生命周期的地基配置项默认值含义gcTime5 分钟5 * 60 * 1000ms查询在失去所有活跃观察者后数据在缓存中保留的时间到期后被垃圾回收staleTime0即立即过期数据保持新鲜fresh的时长一旦超过即被视为过期stale并允许触发后台重新请求注意区分gcTime决定缓存数据能活多久从无人使用那一刻开始倒计时staleTime决定缓存数据何时被认为过期需要刷新。二者彼此独立。从源码可以确认gcTime默认值并非写死在某个常量里而是由 Removable.ts 中的updateGcTime方法动态决定的// packages/query-core/src/removable.ts protected updateGcTime(newGcTime: number | undefined): void { // Default to 5 minutes (Infinity for server-side) if no gcTime is set this.gcTime Math.max( this.gcTime || 0, newGcTime ?? (isServerEnvironment() ? Infinity : 5 * 60 * 1000), ) }这段实现透露了两个重要事实客户端默认gcTime正是5 分钟5 * 60 * 1000ms服务端渲染场景下默认值为Infinity因为服务端没有必要为一次渲染生命周期内的查询做延迟清理避免不必要的定时器开销。Query 生命周期全景一个[todos]键的完整故事下面的缓存示例展示了一条查询从诞生到消亡的完整时间线涵盖四个关键主题有缓存数据与无缓存数据的 Query 实例Query Instances with and without cache data后台重新请求Background Refetching非活跃查询Inactive Queries垃圾回收Garbage Collection在默认gcTime为5 分钟、默认staleTime为0的前提下故事围绕同一个查询展开// 本文所有阶段共用的查询 useQuery({ queryKey: [todos], queryFn: fetchTodos })阶段一首个实例挂载——无缓存硬加载并首次请求一个useQuery({ queryKey: [todos], queryFn: fetchTodos })的新实例挂载了。由于此前从未有人使用过[todos]这个 query key缓存中不存在对应数据。此时该查询会呈现硬加载hard loading状态status pending/isPending true并发起一次网络请求获取数据。当网络请求完成后返回的数据会被写入缓存存放在[todos]这个 key 之下。数据在经过了配置的staleTime默认0即立刻之后会被标记为过期stale。源码印证在 Preact 适配层useQuery只是一个薄封装它最终将参数与QueryObserver一起交给useBaseQuery见 useQuery.ts// packages/preact-query/src/useQuery.ts export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) { return useBaseQuery(options, QueryObserver, queryClient) }而在 useBaseQuery.ts 中关键的一步是按规范化后的查询哈希去缓存里找或建查询实例const client useQueryClient(queryClient) const defaultedOptions client.defaultQueryOptions(options) const query client .getQueryCache() .get...(defaultedOptions.queryHash)client.defaultQueryOptions(options)会把传入的queryKey序列化成queryHash并合并全局默认配置随后queryCache.get(queryHash)负责查找——找不到时缓存内部就会创建一条新的 Query。这正是同一queryKey对应同一份缓存的机制源头缓存索引的粒度是规范化后的queryKey而不是 queryFn 函数本身。阶段二第二个实例挂载——命中缓存触发后台刷新另一个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例在别处挂载了。因为缓存中已经有了第一次查询写入的[todos]数据该数据会被立即返回给这个新实例UI 无需再次白屏等待。与此同时由于默认staleTime为0这份数据已被判定为过期新实例会用它的 queryFn 触发一次新的网络请求即后台重新请求。值得特别强调无论两个组件的fetchTodosqueryFn 是否完全相同只要 query key 一致它们对应的两个查询的status都会被同步更新包括isFetching、isPending等相关状态因为二者共享同一个缓存条目。当请求成功完成后缓存中[todos]下的数据会被更新为新数据两个实例也会同时收到新数据并触发各自的组件更新。原理拆解这里的共享并非巧合。QueryObserver订阅的是同一条Query实例——第一个组件与第二个组件通过queryCache.get(queryHash)拿到的是缓存里的同一个 Query。当 Query 状态变化例如fetchStatus变为fetching、dataUpdatedAt更新时QueryObserver会收到通知并逐一向订阅它的组件广播。在 Preact 适配层组件通过 useBaseQuery.ts 中的useSyncExternalStore完成订阅并将多个通知通过notifyManager.batchCalls批量合并以避免多余渲染。因此第二个实例挂载时它经历了读缓存立即渲染成功态 → 因 stale 触发后台 fetchisFetching变为true但已渲染的数据仍保留不会闪回 loading→ 新数据到达后整体刷新。这正是缓存先行、后台补新的体验模式。阶段三两个实例先后卸载——查询转为非活跃垃圾回收倒计时开始两个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例都卸载了不再有任何组件使用该查询。由于这条查询不再有活跃实例active instances系统会按照gcTime设置一个垃圾回收定时器到期后删除并回收这条查询默认5 分钟。源码印证Removable基类的 scheduleGc 实现了这一逻辑protected scheduleGc(): void { this.clearGcTimeout() if (isValidTimeout(this.gcTime)) { this.#gcTimeout timeoutManager.setTimeout(() { this.optionalRemove() }, this.gcTime) } }当 Query 最后一个 observer 退订、观察者数量归零时Query 会被标记为 inactive随即调用scheduleGc()启动倒计时。需要澄清一个常见误区非活跃不等于被删除。失去所有活跃实例的查询仍会保留在缓存中以便后续被再次使用时能秒开这一点在 Important Defaults 中有明确说明。只有当gcTime倒计时走完且期间无人重新订阅时optionalRemove()才会真正把数据从缓存中移除。阶段四gcTime 倒计时内重新挂载——读缓存 后台刷新在缓存超时gcTime尚未结束时又一个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例挂载了。该查询会立即返回缓存中的可用数据同时在后台执行fetchTodos函数。当后台请求成功完成后缓存会被填充上最新数据。这正是gcTime的意义所在只要数据还在回收宽限期内用户回到该页面/组件就能零等待看到旧数据随后由后台刷新静默替换成新数据。这个阶段的行为与阶段二本质相同——区别只在于阶段二是多个实例同时活跃触发刷新的去重逻辑而这里是一个全新的 observer 重新建立订阅触发 stale 状态下的自动重新请求。阶段五最后一个实例卸载且 5 分钟内无人再用——数据被回收最后一个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例卸载了。之后的5 分钟内没有再出现任何使用[todos]key 的查询实例。于是缓存中[todos]下的数据被删除并完成垃圾回收。至此一条查询的生命周期画上句号无数据时硬加载 → 写缓存 → 同 key 共享读缓存/后台刷新 → 全部卸载后进入 gcTime 倒计时 → 到期删除。完整时间线一览把上述五个阶段压缩成一张时序表便于整体把握时间点事件缓存状态UI 表现T1首个实例挂载无[todos]缓存硬加载isPending随后展示数据T2第二个实例挂载数据已 stale命中缓存立即显示缓存数据isFetching为 true后台刷新T3刷新完成缓存更新为新数据两个实例同时收到新数据T4两个实例卸载数据仍在缓存开始 gcTime 倒计时无观察者不触发渲染T5倒计时内新实例挂载命中缓存秒开缓存数据 后台静默刷新T6最终实例卸载且 5 分钟内无新订阅数据被删除、回收—关键 API 与状态语义补充生命周期中出现的status与fetchStatus需要分开理解详见 useQuery 参考文档status数据维度pending无数据可展示、error最近一次请求失败、success有数据可展示fetchStatus请求维度fetching正在请求包括后台刷新、idle、paused网络离线/被暂停。因此阶段二/阶段四中的后台刷新体现为status保持success的同时fetchStatus变为fetching对应result.isFetching true——组件若用isFetching做指示就能渲染出Background Updating...这类非阻塞提示Preact 的useQueryJSDoc 示例中就有{isFetching ? Background Updating... : }的用法。实战调优掌控缓存的两个旋钮官方指南强调理解默认行为之后绝大多数缓存调优都落在两个选项上它们既可以在QueryClient上全局配置也可以在每个useQuery调用上局部覆盖全局配置所有查询共享默认值import { QueryClient, QueryClientProvider } from tanstack/preact-query const queryClient new QueryClient({ defaultOptions: { queries: { staleTime: 2 * 60 * 1000, // 2 分钟内读缓存、不主动刷新 gcTime: 10 * 60 * 1000, // 无人订阅后保留 10 分钟再回收 }, }, }) export function App() { return ( QueryClientProvider client{queryClient} {/* 组件树 */} /QueryClientProvider ) }局部覆盖仅作用于某条查询const { data, isFetching } useQuery({ queryKey: [todos], queryFn: fetchTodos, staleTime: 60 * 1000, // 该查询 1 分钟内视为新鲜 gcTime: 5 * 60 * 1000, // 失活后 5 分钟回收 })对staleTime的几个取值档位来源见 Important Defaults实践中可按数据变化频率选择设一个有限值如2 * 60 * 1000该时长内直接读缓存、不触发任何刷新直到查询被手动失效设为Infinity永不因过期而自动刷新但仍可被invalidateQueries()手动失效后重新请求设为static彻底禁止重取——即使手动失效也无效refetchOnMount/refetchOnWindowFocus/refetchOnReconnect设为always同样被其拦截。适合运行时不可能变化的数据例如启动时拉取的 feature flags、登录时加载的用户权限、静态参照表。而调整gcTime时需留意updateGcTime中的Math.max(this.gcTime || 0, newGcTime ?? ...)语义gcTime只会在已有值与新值之间取更大者因此无法把某条已存在的查询的gcTime调得更短只能延长。何时触发后台刷新与缓存生命周期相邻的行为理解了缓存生命周期后还需知道 stale 数据在哪些时间点会被自动刷新详见 Important Defaults 与各专题指南新的查询实例挂载时refetchOnMount——本文阶段二、四的核心触发源浏览器窗口重新获得焦点时refetchOnWindowFocus见 window-focus-refetching网络重新连接时refetchOnReconnect。另外refetchInterval提供的周期性轮询与staleTime相互独立详见 polling而失败的请求默认会以指数退避静默重试 3 次后才把错误抛给 UI由默认retry: 3与指数退避retryDelay控制。这些默认项叠加起来构成了打开页面 → 秒看旧数据 → 焦点切换后后台补新 → 失败静默重试的完整缓存体验。小结通过这一条[todos]查询的完整旅程可以看到TanStack Query 的缓存模型本质上是三件套协同工作QueryCache queryHash以规范化 query key 为唯一索引的共享存储见 QueryCache 参考保证跨组件的去重与共享QueryObserver 订阅机制让多个组件实例共享同一条 Query 的状态变化实现一份缓存、多处实时同步staleTime gcTime前者控制何时需要重新请求后者控制无人使用后数据存活多久二者共同把网络请求的时机从手动useEffect中解放出来。理解这套生命周期之后你在 Preact 应用里遇到的为什么这个查询会闪 loading为什么离开了又回来数据还在为什么缓存永远不清空等问题都可以回归到本文的五个阶段逐一排查。若要进一步深挖关联行为可继续阅读 query-invalidation、prefetching 与 initial-query-data 等指南。【免费下载链接】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),仅供参考