news 2026/9/11 5:38:45

TanStack Query 的 QueryCache 完全指南:查询缓存存储机制、实例查找与订阅实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Query 的 QueryCache 完全指南:查询缓存存储机制、实例查找与订阅实战

TanStack Query 的 QueryCache 完全指南:查询缓存存储机制、实例查找与订阅实战

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

QueryCache 是 TanStack Query 体系中所有查询数据的统一存储层,负责保存每个查询的数据、元信息与完整状态。本篇指南以 docs/reference/QueryCache.md 为核心骨架,结合仓库中 packages/query-core/src/queryCache.ts 的源码实现与 packages/query-core/src/tests/queryCache.test.tsx 的测试用例,带你完整掌握 QueryCache 的构造选项、find/findAll/subscribe/clear四大方法,以及它底层的存储、通知与查询生命周期机制。读完本文,你将能够在实际项目中熟练地直接操纵查询缓存实例、精准筛选查询、监听缓存事件并实现自定义的缓存策略。

QueryCache 是什么:TanStack Query 的查询存储中枢

QueryCache是 TanStack Query 的存储机制(storage mechanism),它保存了其内部所有查询(Query)的数据、元信息(meta information)和状态(state)。从源码看,它的本质是一个以queryHash为键、以Query实例为值的 Map 容器:

// packages/query-core/src/queryCache.ts export class QueryCache extends Subscribable<QueryCacheListener> { #queries: QueryStore constructor(public config: QueryCacheConfig = {}) { super() this.#queries = new Map<string, Query>() } }

需要注意的关键结论是:通常情况下你不需要直接与 QueryCache 交互,而是通过QueryClient来操作特定的缓存。在 React、Vue、Svelte、Solid、Angular、Lit、Preact 等各框架适配层中,开发者日常使用的useQueryqueryClient.setQueryDataqueryClient.invalidateQueries等方法,最终都会落到这个核心类上。

QueryCache 继承自Subscribable<QueryCacheListener>(packages/query-core/src/subscribable.ts),因此它天然具备"订阅-通知"能力,这也是后面subscribe方法能够工作的基础。

创建 QueryCache 实例与配置选项

你可以在各框架包中导入QueryCache并直接实例化:

import { QueryCache } from '@tanstack/react-query' const queryCache = new QueryCache({ onError: (error) => { console.log(error) }, onSuccess: (data) => { console.log(data) }, onSettled: (data, error) => { console.log(data, error) }, }) const query = queryCache.find({ queryKey: ['posts'] })

注意:不同框架包的导出入口不同,例如@tanstack/vue-query@tanstack/svelte-query也会导出各自的 QueryCache。所有框架适配层共享同一份query-core实现。

构造选项(Options)

根据 queryCache.ts 中的QueryCacheConfig类型定义,构造选项如下:

选项类型必填说明
onError(error: unknown, query: Query) => void当某个查询发生错误时被调用
onSuccess(data: unknown, query: Query) => void当某个查询成功时被调用
onSettled(data: unknown \| undefined, error: unknown \| null, query: Query) => void当某个查询落定(无论成功还是失败)时被调用

这三个回调是全局级的:无论缓存中哪个查询发生状态变化,只要命中对应的终态就会触发。它们的调用时机可以在 packages/query-core/src/query.ts 中找到确切的实现证据——在查询成功分支中依次调用cache.config.onSuccesscache.config.onSettled,在错误分支中依次调用cache.config.onErrorcache.config.onSettled(错误分支还会把错误rethrow以便上层继续处理)。

对应的测试用例位于 packages/query-core/src/tests/queryCache.test.tsx,验证了:

  • 查询出错时onError被调用一次、onSuccess不被调用、onSettled被调用一次且携带(undefined, error, query)
  • 查询成功时onSuccess被调用一次并携带(data, query)onError不被调用、onSettled携带(data, null, query)

如何让 QueryClient 使用自定义 QueryCache

QueryCache 通常作为QueryClient的内部成员存在(通过queryClient.getQueryCache()获取)。你可以在创建 QueryClient 时注入自定义的 QueryCache:

import { QueryCache, QueryClient } from '@tanstack/react-query' const queryCache = new QueryCache({ onError: (error) => console.error('全局查询错误:', error), }) const queryClient = new QueryClient({ queryCache })

这样做可以让全局错误处理等逻辑与查询客户端实例解耦。仓库测试中同样大量使用该模式,例如在 queryCache.test.tsx 中通过new QueryClient({ queryCache: testCache })注入独立缓存来做隔离测试。

queryCache.find:按精确条件同步查找查询实例

find是一个相对高级的同步方法,用于从缓存中获取已存在的查询实例。返回的实例不仅包含查询的全部状态,还包含查询的所有实例(observers)及其底层内部结构。如果查询不存在,则返回undefined

const query = queryCache.find({ queryKey })

注意:大多数应用通常不需要用到它,但在某些少见场景下,当需要获取某个查询的更多信息时会很有用。例如查看query.state.dataUpdatedAt时间戳,来判断该查询的数据是否足够新鲜、能否直接作为初始值使用。

参数说明

  • filters: QueryFilters:完整的筛选器定义见 Query Filters,其中queryKey: QueryKey是必填项(见 Query Keys)。
  • 返回值:Query(缓存中的查询实例)或undefined

源码级原理:为什么 find 是精确匹配

从源码看,find的实现会强制把exact默认置为true,再做一次线性扫描匹配:

// packages/query-core/src/queryCache.ts find<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData>( filters: WithRequired<QueryFilters, 'queryKey'>, ): Query<TQueryFnData, TError, TData> | undefined { const defaultedFilters = { exact: true, ...filters } return this.getAll().find((query) => matchQuery(defaultedFilters, query), ) as Query<TQueryFnData, TError, TData> | undefined }

也就是说,find默认执行的是精确匹配(基于 queryHash 的哈希比较,见 packages/query-core/src/utils.ts 中matchQueryexact的处理),这与findAll默认的"部分前缀匹配"语义形成鲜明对比。

queryCache.findAll:按筛选条件获取查询实例集合

findAll是更进一步的同步方法,用于从缓存中获取部分匹配查询键的查询实例。如果没有任何查询匹配,则返回空数组。

const queries = queryCache.findAll({ queryKey })

参数说明

  • filters?: QueryFilters:可选,完整定义见 Query Filters。不传时返回缓存中全部查询。
  • 返回值:Query[](缓存中所有匹配的查询实例数组)。

QueryFilters 支持的全部字段

根据 packages/query-core/src/utils.ts 的类型定义与 docs/framework/react/guides/filters.md,筛选器支持以下属性:

属性类型说明
queryKeyQueryKey \| TuplePrefixes<QueryKey>设置要匹配的查询键
exactboolean若设为true,只返回查询键完全一致的查询(否则按前缀/部分匹配)
type'active' \| 'inactive' \| 'all'默认allactive匹配活跃查询,inactive匹配非活跃查询
stalebooleantrue匹配过期查询,false匹配新鲜查询
fetchStatusFetchStatusfetching匹配正在请求的查询;paused匹配想请求但被暂停的查询;idle匹配未在请求的查询
predicate(query: Query) => boolean自定义谓词函数,作为最终过滤条件;若未指定其他筛选器,则对缓存中每个查询求值

源码级原理:空筛选返回全部

findAll的实现非常直接——遍历全部查询,用matchQuery逐个过滤:

// packages/query-core/src/queryCache.ts findAll(filters: QueryFilters<any> = {}): Array<Query> { const queries = this.getAll() return Object.keys(filters).length > 0 ? queries.filter((query) => matchQuery(filters, query)) : queries }

注意:当传入空对象{}(或完全不传)时,它直接返回全部查询,不做匹配计算。

实测行为验证(来自仓库测试)

queryCache.test.tsx 中有一个非常全面的findAll过滤测试,可以作为行为参考:

  • findAll({ queryKey: key1 })返回且仅返回 key1 对应的查询;
  • 由于 v4 起查询键必须是数组,额外再包一层数组findAll({ queryKey: [key1] })会得到空数组;
  • findAll()findAll({})返回全部 4 个查询;
  • type: 'inactive'/type: 'active'可按活跃状态过滤(无活跃 observer 的查询为 inactive);
  • stale: true/stale: false可按是否过期过滤;
  • exact: trueexact: false(默认)控制查询键是精确匹配还是部分匹配——例如用{ a: 'a' }部分匹配[{ a: 'a', b: 'b' }]时,exact: true返回空,exact: false能命中;
  • predicate: (query) => query === query3可用自定义谓词精确挑选;
  • fetchStatus: 'idle'/fetchStatus: 'fetching'可按请求状态过滤。

实战:用 subscribe + findAll 实现缓存上限控制

仓库测试中展示了一个非常实用的模式——通过subscribe监听added事件,配合findAll限制缓存大小(只保留最近 2 个查询):

const testCache = new QueryCache() const unsubscribe = testCache.subscribe((event) => { if (event.type === 'added') { if (testCache.getAll().length > 2) { testCache .findAll({ type: 'inactive', predicate: (q) => q !== event.query, }) .forEach((query) => { testCache.remove(query) }) } } }) const testClient = new QueryClient({ queryCache: testCache })

完整代码见 queryCache.test.tsx。该用例最终验证缓存中只剩最新添加的data3,说明"事件驱动 + 过滤 + 移除"是可行的自定义缓存治理方案。

queryCache.subscribe:订阅整个缓存的更新事件

subscribe方法用于订阅整个查询缓存,并收到缓存发生的安全/已知更新通知,例如查询状态改变、查询被新增、更新或移除。

const callback = (event) => { console.log(event.type, event.query) } const unsubscribe = queryCache.subscribe(callback)

参数与返回值

  • callback: (event: QueryCacheNotifyEvent) => void:每当缓存通过其受追踪的更新机制(如query.setStatequeryClient.removeQueries等)被更新时,该函数都会被调用。对缓存进行的"计划外"(out of scope)变更是不被鼓励的,且不会触发订阅回调。
  • 返回值:unsubscribe: Function => void,调用它即可取消订阅。

事件类型:源码定义

根据 queryCache.ts 中QueryCacheNotifyEvent的联合类型定义,订阅者可能收到以下 7 种事件:

事件 type携带负载含义
addedquery查询被加入缓存
removedquery查询被移出缓存
updatedquery,action查询状态被更新(携带触发更新的 Action)
observerAddedquery,observer有新 observer 订阅该查询
observerRemovedquery,observer有 observer 取消订阅该查询
observerResultsUpdatedqueryobserver 的结果被更新
observerOptionsUpdatedquery,observerobserver 的选项被更新

源码级原理:notify 与批量通知

所有通知最终都汇聚到notify方法,它使用notifyManager.batch将同一批次内的多次通知合并执行,避免中间状态导致重复渲染:

// packages/query-core/src/queryCache.ts notify(event: QueryCacheNotifyEvent): void { notifyManager.batch(() => { this.listeners.forEach((listener) => { listener(event) }) }) }

notifyManager的实现见 packages/query-core/src/notifyManager.ts,它的批量调度机制是 TanStack Query 高性能通知体系的核心。

一个查询完整生命周期中产生的事件序列

仓库测试 queryCache.test.tsx 记录了一个查询从创建到过期的完整事件序列:

1. added // 查询加入缓存 -> loading 2. observerResultsUpdated // observer 结果更新 -> loading 3. observerAdded // observer 加入 4. observerResultsUpdated // observer 结果更新 -> fetching 5. updated // 查询状态更新 -> fetching 6. observerResultsUpdated // observer 结果更新 -> success 7. updated // 查询状态更新 -> success 8. observerResultsUpdated // observer 结果更新 -> stale

该测试还断言了事件序列中出现的event.query始终是同一个缓存实例,验证了缓存实例的唯一性。

queryCache.clear:清空整个缓存

clear方法用于完全清空缓存,重新开始。

queryCache.clear()

从源码看,clear会遍历所有查询并逐个remove,且整个过程被包裹在notifyManager.batch中,保证只触发一次批量通知:

// packages/query-core/src/queryCache.ts clear(): void { notifyManager.batch(() => { this.getAll().forEach((query) => { this.remove(query) }) }) }

remove内部会先调用query.destroy()释放 observer 等资源,再从 Map 中删除并发出removed事件(见 queryCache.ts)。注意remove的删除是"按实例校验"的:只有当前存储在该 queryHash 下的实例才会被真正删除,这一点在测试 queryCache.test.tsx 中有明确验证。

源码纵览:QueryCache 的完整内部机制

为了更深入地理解,这里梳理 QueryCache 除公开方法外的几个关键内部成员(均位于 packages/query-core/src/queryCache.ts):

  • build(client, options, state?):核心的查询构建入口。它先用options.queryHash ?? hashQueryKeyByOptions(queryKey, options)计算哈希(可自定义queryKeyHashFn,默认使用 utils.ts 中的hashKey——基于 JSON 序列化并对普通对象按键排序的稳定哈希),若缓存中已存在同哈希查询则直接复用,否则创建新的Queryadd进缓存。这保证了同一查询键在缓存中只有一个实例
  • get(queryHash)/getAll():分别返回单个查询(按哈希)与全部查询数组,find/findAll均基于getAll实现。
  • add(query):仅当哈希不存在时才写入 Map 并发出added事件;重复添加同一哈希的查询不会生效(测试 queryCache.test.tsx 验证了缓存长度仍为 1)。
  • onFocus()/onOnline():批量地将窗口聚焦、网络恢复事件转发给缓存内的每个查询,用于驱动"重新聚焦即刷新""恢复在线即刷新"等默认行为(配合refetchOnWindowFocusrefetchOnReconnect选项)。
  • QueryStore:内部存储接口,抽象了has/set/get/delete/values操作,目前以原生Map<string, Query>实现,键为queryHash

一条查询数据的完整流转链路

把以上机制串起来,可以画出一次典型查询请求的完整调用链:

useQuery / queryClient.query │ ▼ QueryClient ──► queryCache.build(client, options) // 计算 queryHash,命中缓存或新建 Query │ // (queryCache.ts L100-L131) ▼ Query.fetch / setState ──► 缓存内查询状态更新 │ ├──► cache.config.onSuccess / onError / onSettled // 全局回调 (query.ts L572-L616) │ └──► cache.notify({ type: 'updated', ... }) // 通知订阅者 (queryCache.ts L200-L206) │ ▼ QueryObserver ──► 框架层 (React/Vue/Svelte/Solid/Angular...) ──► UI 更新

这个链路说明:QueryClient是面向开发者的门面,QueryCache是实际的数据仓库与事件源,Query是存储的最小单元,QueryObserver则是连接缓存与 UI 的桥梁。

进一步阅读

  • 要更深入地理解 QueryCache 的内部工作原理,官方推荐阅读 TkDodo 的《Inside React Query》系列文章(见原文档 Further reading 部分)。
  • 想了解查询键的哈希与匹配规则,可阅读 Query Keys 指南 与 Filters 指南。
  • 想了解与 QueryCache 平行的变更缓存,可阅读 MutationCache 参考文档。
  • 想直接阅读源码与测试,可前往 packages/query-core/src/queryCache.ts 与 packages/query-core/src/tests/queryCache.test.tsx,以及依赖的 Query、QueryClient、notifyManager、subscribable、utils 等核心模块。

小结

QueryCache 是整个 TanStack Query 生态的地基:它用 Map 统一管理所有查询实例,通过find/findAll提供精确与模糊两种查询检索能力,通过subscribeadded/removed/updated/observer*等 7 类事件广播给监听者,通过clear一键重置,并借助onSuccess/onError/onSettled提供全局生命周期钩子。日常开发中你几乎不会直接触碰它——但当你需要做缓存预取判断、自定义缓存淘汰策略、全局错误上报或深度调试查询状态时,理解并善用 QueryCache 将成为你手中最有力的武器。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:35:26

ESP32-S3端云架构实战:打造稳定可迭代的AI陪伴设备

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:34:12

Linux入门指南:从零掌握基础命令与系统管理

1. Linux初体验&#xff1a;从零开始的系统认知第一次接触Linux系统时&#xff0c;那种既熟悉又陌生的感觉至今记忆犹新。与Windows不同&#xff0c;Linux给我的第一印象是简洁高效——没有华丽的图形界面&#xff0c;只有一个等待输入命令的终端窗口。作为开源操作系统的代表&…

作者头像 李华
网站建设 2026/9/11 5:34:06

2026国产实时计算平台选型指南:从Flink到湖仓管控全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:33:14

CMSIS-5架构决策地图:五层依赖、工具链治理与选型避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:31:05

SQL Server分页查询性能优化五大方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:29:15

Flutter鸿蒙跨端开发实战:技术选型与适配经验总结

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华