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 等各框架适配层中,开发者日常使用的useQuery、queryClient.setQueryData、queryClient.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.onSuccess与cache.config.onSettled,在错误分支中依次调用cache.config.onError与cache.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 中matchQuery对exact的处理),这与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,筛选器支持以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
queryKey | QueryKey \| TuplePrefixes<QueryKey> | 设置要匹配的查询键 |
exact | boolean | 若设为true,只返回查询键完全一致的查询(否则按前缀/部分匹配) |
type | 'active' \| 'inactive' \| 'all' | 默认all;active匹配活跃查询,inactive匹配非活跃查询 |
stale | boolean | true匹配过期查询,false匹配新鲜查询 |
fetchStatus | FetchStatus | fetching匹配正在请求的查询;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: true与exact: 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.setState、queryClient.removeQueries等)被更新时,该函数都会被调用。对缓存进行的"计划外"(out of scope)变更是不被鼓励的,且不会触发订阅回调。- 返回值:
unsubscribe: Function => void,调用它即可取消订阅。
事件类型:源码定义
根据 queryCache.ts 中QueryCacheNotifyEvent的联合类型定义,订阅者可能收到以下 7 种事件:
| 事件 type | 携带负载 | 含义 |
|---|---|---|
added | query | 查询被加入缓存 |
removed | query | 查询被移出缓存 |
updated | query,action | 查询状态被更新(携带触发更新的 Action) |
observerAdded | query,observer | 有新 observer 订阅该查询 |
observerRemoved | query,observer | 有 observer 取消订阅该查询 |
observerResultsUpdated | query | observer 的结果被更新 |
observerOptionsUpdated | query,observer | observer 的选项被更新 |
源码级原理: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 序列化并对普通对象按键排序的稳定哈希),若缓存中已存在同哈希查询则直接复用,否则创建新的Query并add进缓存。这保证了同一查询键在缓存中只有一个实例。get(queryHash)/getAll():分别返回单个查询(按哈希)与全部查询数组,find/findAll均基于getAll实现。add(query):仅当哈希不存在时才写入 Map 并发出added事件;重复添加同一哈希的查询不会生效(测试 queryCache.test.tsx 验证了缓存长度仍为 1)。onFocus()/onOnline():批量地将窗口聚焦、网络恢复事件转发给缓存内的每个查询,用于驱动"重新聚焦即刷新""恢复在线即刷新"等默认行为(配合refetchOnWindowFocus、refetchOnReconnect选项)。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提供精确与模糊两种查询检索能力,通过subscribe将added/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),仅供参考