
Vue Query 缓存机制全解析staleTime、gcTime 与查询缓存的生命周期管理【免费下载链接】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 系列框架含tanstack/vue-query的核心竞争力在于一套开箱即用的服务端状态缓存体系同一份数据通过queryKey被多个组件共享数据在缓存中保鲜、失效、被后台刷新最终在无人使用时被垃圾回收。本文以官方指南 Caching Examples 为骨架该指南在仓库中以ref方式复用 React 版完整内容结合packages/query-core与packages/vue-query的源码实现带你完整走一遍缓存从写入、共享、后台刷新到垃圾回收的全过程并深入理解staleTime数据新鲜度与gcTime垃圾回收时间两大核心配置读完即可在生产环境中精准控制数据请求频率与缓存占用量。在阅读本文之前建议先通读 Important Defaults重要默认值——TanStack Query 的默认行为激进但合理理解这些默认值能避免大多数使用上的困惑。缓存机制的底层模型在深入生命周期示例之前先建立两个关键认知1.queryKey是缓存的唯一标识。缓存本身并不理解你的查询函数它只按序列化后的queryKey查找、写入和更新数据。两个组件只要传入相同的queryKey就会命中同一条缓存记录Query实例。因此queryKey必须包含查询的所有变量如筛选条件、分页页码、详情 ID详见 Query Keys 指南。2. 所有Query实例统一存放在QueryCache中。在 packages/query-core/src/queryCache.ts 中缓存以Map结构按queryHash组织并对外发出added、updated、removed、observerAdded、observerRemoved等通知事件。而每条查询记录的状态字段定义在 packages/query-core/src/query.ts 的QueryState中包括data/dataUpdatedAt缓存数据及其写入时间戳新鲜度计算的依据statuspending/success/error查询结果状态fetchStatusfetching/idle/paused请求过程状态用于区分加载中与后台刷新isInvalidated是否已被手动失效标记。在 Vue 侧useQuery通过 packages/vue-query/src/useQuery.ts 创建QueryObserver并订阅上述状态packages/vue-query/src/useBaseQuery.ts 再把观察结果包装成 Vue 的响应式ref返回并在组件作用域销毁时自动取消订阅。理解了这一模型下面的生命周期示例就顺理成章了。基础示例一条查询缓存的完整生命周期假设我们使用默认的gcTime5 分钟和默认的staleTime0即数据一经写入立即视为过期一条todos查询从创建到销毁会经历以下完整过程。第一步第一个查询实例挂载第一个组件挂载并调用script setup import { useQuery } from tanstack/vue-query const { isPending, data } useQuery({ queryKey: [todos], queryFn: fetchTodos, }) /script由于此前从未有人以[todos]作为queryKey发起过查询缓存中不存在对应记录此时查询会进入硬加载状态isPending true页面上需要显示 Loading并发起一次真实的网络请求网络请求完成后返回的数据被写入缓存存放在[todos]这个 key 下dataUpdatedAt被标记为当前时间由于staleTime默认为0该数据会被立即标记为过期stale。第二步第二个查询实例挂载缓存命中 后台刷新另一个组件在页面的其他位置也挂载了同一条查询script setup import { useQuery } from tanstack/vue-query const { isPending, data, isFetching } useQuery({ queryKey: [todos], queryFn: fetchTodos, }) /script由于缓存中已有[todos]的数据第二个实例立即从缓存拿到数据无需等待网络页面无需重新进入 Loading 状态但该数据已过期staleTime: 0因此新实例会触发一次后台网络请求使用它自己的queryFn重新拉取数据关键细节无论两个实例的fetchTodos函数是否完全相同因为它们的queryKey相同两个实例的status包括isFetching、isPending等相关状态会同步更新——这就是 Queries 指南 中展示的isFetching与isPending区别的典型场景已有缓存时isPending为false但后台刷新期间isFetching为true当请求成功后缓存中[todos]的数据被新数据覆盖两个实例同时收到新数据并触发响应式更新。第三步所有实例卸载进入 inactive 状态两个实例都被卸载、不再被使用由于该查询已没有活跃的观察者observer它被标记为inactive不活跃并基于gcTime启动一个垃圾回收定时器默认5 分钟注意进入 inactive 的查询并不会立即从缓存中删除数据仍然保留在缓存中以备将来再次使用。这也是离开页面再回来时能够秒开的原因之一。第四步gcTime 到期前重新挂载缓存复活如果在这 5 分钟窗口期内又一个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例挂载查询立即返回缓存中的旧数据同时fetchTodos在后台重新执行完成后用新数据填充缓存并刷新所有活跃实例由于查询重新变得活跃之前设置的垃圾回收定时器会被取消。第五步最终卸载且无人问津缓存被回收最后一个实例卸载后如果5 分钟内再也没有任何实例使用[todos]这个 key该 key 下的缓存数据被删除并完成垃圾回收此后若再发起同 key 查询将回到第一步的硬加载 网络请求流程。这套流程解释了 TanStack Query 缓存机制最精髓的部分缓存命中带来极致的用户体验后台刷新保证数据最终一致性inactive gcTime 机制则避免缓存无限膨胀。staleTime掌控数据新鲜度staleTime决定一条查询的数据在多长时间内被视为新鲜fresh从而免于触发基于过期的重新拉取。默认值为0立即过期但你可以按需调整取值行为0默认数据一经写入立即视为过期所有重新挂载、窗口聚焦、网络重连都可能触发后台 refetch毫秒数如2 * 60 * 10002 分钟内数据保持新鲜直接读缓存、不触发任何 refetch直到 2 分钟过去或被 手动失效Infinity永不因过期触发 refetch但被 手动失效 时仍会重新拉取static永不触发 refetch即使被手动失效也不会重新拉取在源码层面新鲜度判断实现在 packages/query-core/src/query.ts 的isStaleByTime中逻辑非常直白没有数据永远是过期的state.data undefined直接返回truestaleTime static时永不视为过期return false查询被失效isInvalidated时视为过期否则通过timeUntilStale(dataUpdatedAt, staleTime)判断——该工具函数定义在 packages/query-core/src/utils.ts即Math.max(updatedAt staleTime - Date.now(), 0)剩余时间为 0 即过期。static与Infinity的关键区别在于失效的严格程度queryClient.invalidateQueries()可以让staleTime: Infinity的查询重新拉取但对staleTime: static完全无效同时refetchOnMount、refetchOnWindowFocus、refetchOnReconnect设置为always时也会被static拦截这一点由 query.ts 中的isStatic()方法支持。因此应用运行期间不可能变化的数据如启动时拉取的功能开关、登录时加载的用户权限、静态参考表适合用static可能变化、但你希望只在手动失效时更新的数据如由 mutation 驱动的列表刷新适合用Infinity以便保留手动失效能力。另外提醒一点staleTime只控制是否会因过期而重新拉取而周期性轮询由独立的refetchInterval控制两者互不影响详见 Polling 指南。gcTime垃圾回收与缓存占用控制gcTime控制 inactive 查询在缓存中保留的时间默认5 分钟1000 * 60 * 5毫秒。它的底层实现位于 packages/query-core/src/removable.tsupdateGcTime()合并设置 gcTime未显式配置时客户端默认 5 分钟、服务端SSR默认为Infinity且新值只会取更大的那一个Math.maxscheduleGc()查询变 inactive 时启动定时器到期调用optionalRemove()从缓存中移除查询一旦重新变得活跃定时器会被立即清除。这一实现直接支撑了前面生命周期示例的第三、四、五步。你可以通过以下方式调整默认值// 全局配置在创建 QueryClient 时 import { VueQueryPlugin } from tanstack/vue-query const queryClient new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 10, // 10 分钟后回收 inactive 查询 }, }, })// 单条查询配置 useQuery({ queryKey: [todos], queryFn: fetchTodos, gcTime: 1000 * 60 * 1, // 这条查询 1 分钟后回收 })合理设置gcTime的意义在于值越大离开再回来时越容易命中缓存、体验越好但内存与持久化占用越高值越小缓存越精简但重复挂载可能回到硬加载状态。对于超大响应体或长列表数据可以适当调低gcTime以控制内存对于高频返回的小数据保持默认值即可。后台重新拉取Background Refetching的触发时机默认情况下过期stale的查询会在以下时机被自动后台刷新新的查询实例挂载即上文的第二步窗口重新获得焦点详见 Window Focus Refetching网络重新连接详见 Network Mode。如果你希望精细控制这些触发点可以单独配置refetchOnMount、refetchOnWindowFocus、refetchOnReconnect但官方推荐的最简单做法仍然是用staleTime拉开数据保鲜窗口从根源上减少无谓的 refetch 次数。关于默认值的完整清单与说明请参阅 Important Defaults。Vue Query 的响应式注意点同为 TanStack Query 家族tanstack/vue-query的缓存生命周期与 React 版完全一致但 API 形态上有几点 Vue 特性值得留意useQuery返回的data、isPending、isFetching、status等都是Vue 响应式 ref在模板中会自动解包但在script setup组合式代码中需要写.value详见 Queries 指南 的模板示例选项支持MaybeRef/MaybeRefOrGetterqueryKey可以直接传入一个Ref例如const queryKey [todos, todoId]其中todoId是Refstring当 ref 值变化时查询会自动以新 key 重新发起——这正是 Query Keys 指南 中useTodos(todoId: Refstring)示例的用法useQuery必须在组件setup()或运行中的 effect 作用域内调用组件卸载时 useBaseQuery.ts 会通过onScopeDispose自动取消 observer 订阅——订阅数归零正是查询进入 inactive、启动 gcTime 定时器的前提因此无需手动清理。延伸阅读缓存只是服务端状态管理的一环建议继续阅读同目录下的以下指南形成完整知识闭环QueriesuseQuery返回值与模板中status的完整用法Query Keys如何设计稳定且能命中缓存的 keyQuery Invalidation配合 mutation 手动失效缓存Prefetching在进入页面之前预热缓存进一步提升命中率PollingrefetchInterval与staleTime的配合使用Window Focus Refetching聚焦时自动刷新的精细化控制。【免费下载链接】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),仅供参考