ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解 TanStack Query 缓存生命周期:从首次挂载、后台刷新到垃圾回收的完整推演

深入理解 TanStack Query 缓存生命周期:从首次挂载、后台刷新到垃圾回收的完整推演 深入理解 TanStack 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/query本文以官方指南 Caching Examples 为骨架结合 query-core 包中的真实源码与测试用例系统梳理 TanStack QueryReact Query中查询数据的缓存生命周期什么是命中缓存、什么是后台刷新、查询何时被标记为 inactive、垃圾回收Garbage Collection的计时器又是如何被启动与取消的。读完本文你将能准确推演任意一个useQuery实例从挂载到销毁的全过程并能在实践中用gcTime、staleTime、refetchOnMount等选项精确控制缓存行为避免数据迟迟不更新或缓存无限膨胀两类典型问题。阅读前提先理解缓存相关的重要默认值官方在阅读缓存指南之前明确要求先通读 Important Defaults。其中与缓存生命周期强相关的默认值如下它们是后面整段推演的前提通过useQuery/useInfiniteQuery创建的新查询实例默认把缓存数据视为 stale过期设置了staleTime的查询在计时未走完前被视为fresh新鲜期间不会再触发任何基于过期的刷新stale 的查询会在以下三类时机被后台自动重新请求新的查询实例挂载时、浏览器窗口重新获得焦点时、网络重新连接时不再有任何活跃实例observer的查询会被标记为inactive但仍保留在缓存中供后续复用默认情况下inactive 查询会在5 分钟后被垃圾回收可配置gcTime失败的查询默认会以指数退避延迟静默重试 3 次。值得特别留意的是staleTime的两种特殊取值来自 important-defaults.md取值行为是否仍可被invalidateQueries手动失效数字毫秒计时内数据视为 fresh不触发基于过期的刷新可以Infinity永不因过期触发刷新可以手动失效依然生效static永不因过期触发刷新refetchOnMount/refetchOnWindowFocus/refetchOnReconnect设为always也会被屏蔽不可以手动失效无效static适合应用运行期间不可能变化的数据如启动时拉取的功能开关、登录时加载的用户权限、静态参照表而Infinity适合仍然希望通过手动失效来控制刷新的数据。一、一条查询的生命周期五阶段完整推演下文是 caching.md 的核心叙事。假设我们使用默认的gcTime5 分钟与默认的staleTime0即数据一旦写入缓存就立即被视为 stale查询体为useQuery({ queryKey: [todos], queryFn: fetchTodos })阶段 1首个实例挂载——硬加载态与首次网络请求当第一个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例挂载时由于此前从未有人用[todos]这个 key 发起过查询缓存中没有任何对应数据因此该查询会进入硬加载状态hard loading state即status pending并立即发起一次网络请求网络请求成功后返回的数据会被写入缓存挂到[todos]key 之下数据被标记为 stale 的时间点由staleTime决定——默认0意味着写入即过期。这里硬加载的准确定义是缓存里没有数据data undefined而不是正在请求。区分这一点是理解后续阶段的前提。阶段 2第二个实例挂载——缓存命中与后台刷新在应用其他位置第二个useQuery({ queryKey: [todos], queryFn: fetchTodos })实例挂载由于第一个查询已经把数据写入了缓存第二个实例会立即从缓存同步返回已有数据用户看不到闪烁的加载态但因为数据此时是 stalestaleTime默认为 0新实例会用自己的queryFn触发一次新的网络请求在后台完成刷新关键点无论两个实例的fetchTodos函数是否完全相同只要 query key 相同它们就共享同一条缓存记录。两个查询的status及相关值包括isFetching、isPending等都会一起更新因为它们背后是同一个被多个 observer 订阅的 Query 对象当后台请求成功时[todos]key 下的缓存数据被新数据覆盖两个实例同时拿到最新数据并重新渲染。这里的双实例状态联动是 TanStack Query 去重能力deduping的直接体现相同 key 的多个订阅者只对应一个网络请求实例但状态变更会对所有订阅者广播。阶段 3两个实例相继卸载——inactive 与 GC 计时器启动当使用[todos]key 的这两个实例都被卸载、不再有任何活跃订阅者时该查询不再有活跃实例被标记为inactive此时会依据gcTime启动一个垃圾回收计时器默认5 分钟到时后删除并回收这条查询及其缓存数据。注意inactive ≠ 立即删除。缓存仍然保留着数据这是为了让用户返回页面如从列表页跳到详情页再返回时能够瞬间渲染旧数据。阶段 4GC 计时器到期之前重新挂载——缓存复活与后台填充如果在 5 分钟的 GC 计时器走完之前又有一个新的useQuery({ queryKey: [todos], queryFn: fetchTodos })实例挂载查询会立即返回缓存中仍然存在的数据页面秒开同时fetchTodos在后台运行成功完成后会用全新数据填充缓存也同步给当前所有订阅者挂载这个新实例会取消clear垃圾回收计时器——这条查询重新恢复为 active 状态。阶段 5再无实例且超时——缓存被删除并回收最后一个实例卸载后若在5 分钟内再也没有任何[todos]的实例出现计时器触发[todos]key 下的缓存数据被删除并完成垃圾回收下次再有人挂载同 key 的查询时将重新回到阶段 1 的硬加载态——仿佛这条查询从未存在过。二、源码印证缓存生命周期在底层是如何实现的上面的叙事并非文档的抽象描述query-core 中确实存在一整套与之对应的机制。理解这些源码能帮你更精确地预估各种边界场景。1.Query继承RemovablegcTime 与 GC 计时器在 query.ts 中Query类继承自Removable。Removable见 removable.ts定义了gcTime字段与四个核心方法updateGcTime(newGcTime)更新 GC 时间。当没有显式传入gcTime时默认取5 * 60 * 1000毫秒5 分钟而在服务端环境中默认值为Infinity即服务端渲染期间查询不会被垃圾回收见 removable.tsscheduleGc()先用timeoutManager.setTimeout登记一个延迟gcTime的定时器到期后回调optionalRemove()clearGcTimeout()取消已登记的计时器optionalRemove()抽象方法由Query实现。Query.optionalRemove()的实现query.ts包含一个双重守卫protected optionalRemove() { if (!this.observers.length this.state.fetchStatus idle) { this.#cache.remove(this) } }即只有同时满足没有任何 observer 订阅且当前不在请求中时查询才会真正从缓存中移除。这解释了文档阶段 3 的行为即使 GC 计时器已经触发只要查询还在 fetching也不会被删除——相关断言可见测试 should be garbage collected later when unsubscribed and query is fetchingquery.test.tsx。2.addObserver/removeObserveractive 与 inactive 的切换开关查询的活跃与不活跃完全由 observer 数量驱动query.tsaddObserver有新的订阅者加入时调用clearGcTimeout()取消垃圾回收计时器防止查询被回收removeObserver订阅者离开后若 observer 列表已空则调用scheduleGc()启动或重置垃圾回收计时器。值得注意的是Query在构造时query.ts以及每次 fetch 结束后query.ts也会调用scheduleGc()目的是兜底回收从未被订阅过或请求刚结束的查询。测试 queries should be garbage collected even if they never fetchedquery.test.tsx正验证了这一点。3. 关于gcTime的三个实现细节结合 removable.ts 与工具函数 utils.ts 可以确认gcTime: 0计时器立即到期查询在最后一个订阅者离开后会被立刻回收。测试 queries with gcTime 0 should be removed immediately after unsubscribingquery.test.tsx可验证gcTime: InfinityisValidTimeout会返回false其实现要求非负有限数字因此根本不会登记计时器查询永远不会被自动回收gcTime 只会取见过的最长值updateGcTime内部通过Math.max(this.gcTime || 0, newGcTime ?? ...)合并removable.ts。测试 should use the longest garbage collection time it has seenquery.test.tsx验证了这一点同一 key 先后以 100/200/10ms 的gcTime创建查询最终query.gcTime是 200。因此如果你想让数据存活更久用较大gcTime的查询去覆盖较小值是不起作用的——这点在多处挂载同 key 查询时尤其容易踩坑。4. 缓存命中与状态联动的原理QueryCache 与 observer第二个实例立即拿到缓存数据靠的是 QueryCache 的去重build()先按 query key 哈希出queryHash若缓存中已存在相同 hash 的查询则直接复用否则才新建queryCache.ts。而remove()会先调用query.destroy()再删除记录queryCache.ts。多个实例状态一起更新则源于共享订阅多个useQueryobserver 订阅同一个QueryQuery的每次状态变更#dispatch都会批量通知全部 observerquery.ts。5. stale的判定isStaleByTime与timeUntilStale数据是否 stale并不玄学它由 query.ts 的isStaleByTime精确计算isStaleByTime(staleTime: StaleTime 0): boolean { // no data is always stale if (this.state.data undefined) return true // static is never stale if (staleTime static) return false // if the query is invalidated, it is stale if (this.state.isInvalidated) return true return !timeUntilStale(this.state.dataUpdatedAt, staleTime) }判定规则依次为没有数据 → 永远 stalestaleTime static→ 永远 fresh已被手动失效invalidated→ stale无论staleTime多长否则用timeUntilStale(dataUpdatedAt, staleTime)utils.ts即updatedAt staleTime - now判断新鲜度剩余时间。而新实例挂载时是否触发后台刷新由 observer 侧的shouldFetchOnMount决定queryObserver.ts只有查询已有数据、enabled不为false、且staleTime不是static时才会在满足refetchOnMount条件always或非false且查询确实 stale时发起后台刷新。这也解释了阶段 2 为什么第二个实例会先有数据、再后台刷新。三、控制缓存生命周期的核心配置项下面是完整生命周期推演中真正起决定作用的配置项可全局设置也可逐查询覆盖。gcTime缓存数据在 inactive 后保留多久类型number | Infinity默认值1000 * 60 * 55 分钟服务端为Infinity见 removable.ts作用查询失去所有活跃订阅者后缓存数据被垃圾回收前等待的时长典型取值列表详情场景建议保留默认 5 分钟即可数据体积大且易变时可调小如10 * 1000SSR / 预取场景可设Infinity并结合手动清除注意v5 之前这个选项叫cacheTimev5 起更名为gcTime语义上强调的是垃圾回收前的保留期。staleTime数据在多久内被视为 fresh类型number | Infinity | static默认值0缓存数据写入即 stale作用在计时内新实例挂载、窗口聚焦、网络重连都不会触发基于过期的后台刷新典型取值短时数据如价格设几秒常规业务数据设60 * 1000级几乎不变的数据设Infinity或static区别见本文开头的表格refetchOnMount/refetchOnWindowFocus/refetchOnReconnect三者用于覆盖何时可以触发刷新的时机取值一致文档见 important-defaults.md焦点/网络相关细节见 window-focus-refetching.md取值行为true默认仅当查询为 stale 时才刷新false永不因此时机刷新always无论数据是否 fresh 都刷新会被static屏蔽相关但不直接属于生命周期的选项enabled置为false时查询不自动发起请求但仍可能产生订阅initialData/placeholderData分别影响默认状态是否带数据进而影响硬加载态详见 initial-query-data.md 与 placeholder-query-data.md手动失效queryClient.invalidateQueries()会把查询标记为isInvalidated从而绕过staleTime强制使其 stale详见 query-invalidation.md。四、配置示例全局与查询级两种写法全局设置作用于所有查询——通过QueryClient的defaultOptionsimport { QueryClient } from tanstack/react-query const queryClient new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 60 秒内视为新鲜不触发基于过期的刷新 gcTime: 5 * 60 * 1000, // 显式声明 inactive 后保留 5 分钟与默认一致 refetchOnWindowFocus: true, retry: 3, // 失败默认静默重试 3 次指数退避 }, }, })逐查询覆盖精确控制个别数据源的缓存策略// 详情页数据挂载即可用旧数据渲染后台 30 秒内不重复刷新 useQuery({ queryKey: [todos], queryFn: fetchTodos, staleTime: 30 * 1000, gcTime: 10 * 60 * 1000, }) // 启动时加载的功能开关运行期间不允许任何形式的自动刷新 useQuery({ queryKey: [feature-flags], queryFn: fetchFeatureFlags, staleTime: static, }) // 高频易变数据每次窗口聚焦都强制刷新 useQuery({ queryKey: [live-stock], queryFn: fetchStock, refetchOnWindowFocus: always, gcTime: 0, // 组件卸载即丢弃避免堆积过期行情 })还可以使用queryClient.setQueryDefaults(key, options)为某一类 key单独设置默认值核心测试即通过它设置查询级gcTime见 query.test.tsx。五、实战中常见的三个误区误区一把staleTime当成了缓存过期时间。staleTime只决定要不要后台刷新不决定数据是否还在。数据是否被删除只由gcTime决定。设staleTime: Infinity而gcTime默认 5 分钟结果是组件卸载 5 分钟后数据照样被回收下次挂载依然要硬加载。误区二认为多个实例会各发各的请求。相同 key 的查询在 QueryCache 中只会存在一份网络请求也会被去重合并不同实例的差异只在于各自对status/isFetching的渲染。需要展示后台正在刷新时请使用isFetching而非status pending后者只代表首次硬加载参见 background-fetching-indicators.md。误区三卸载后立即gcTime归零就能立刻释放内存可以但要记住optionalRemove的双重守卫查询若仍处于 fetching 状态即使计时器到期也不会被移除query.ts这是为了把进行中的请求结果写入缓存。真正需要强制清理时应调用queryClient.clear()或按 filters.md 中的过滤条件配合queryClient.removeQueries()。结语回顾整段推演一条查询的生命周期 首次硬加载 → 缓存命中 后台刷新 → 全部卸载后进入 inactive 并启动 GC 计时 → 超时前复用则缓存复活 → 超时则删除回收。其底层实现横跨 query.tsobserver 增删与 GC 调度、removable.tsgcTime 语义与默认值、queryCache.ts查询去重与移除与 queryObserver.ts挂载/焦点刷新判定并有 query.test.tsx 中大量定时器用例逐条守护。理解了gcTime保留多久与staleTime多久算新鲜这对互补参数再结合refetchOnMount、refetchOnWindowFocus、refetchOnReconnect三个时机开关你就能为任何数据源设计出既流畅又省流量的缓存策略。【免费下载链接】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),仅供参考
返回列表