TanStack Query(Angular)查询失效实战:用 invalidateQueries 精确标记过期数据并触发后台刷新
本篇围绕 TanStack Query 的 Angular 适配包(@tanstack/angular-query-experimental)讲解查询失效(Query Invalidation)机制:当你知道用户操作已使某些数据过期时,如何通过 QueryClient.invalidateQueries 按前缀、精确匹配或谓词函数标记查询为过期,并默认对活跃查询触发后台刷新。文中结合 query-core 源码给出 invalidateQueries 的完整执行链路与过滤匹配规则,帮助你在 Angular 应用中实现精准、可控的缓存失效。
为什么需要主动失效查询
依赖 staleTime 等待查询自然过期再重新拉取,并不能覆盖所有场景——尤其是当你明确知道某个查询的数据已经因为用户的操作(提交表单、删除条目、切换筛选条件等)而过期时。为此,QueryClient 提供了 invalidateQueries 方法,让你可以智能地把查询标记为过期(stale),并在必要时立即触发后台刷新。
说明:一些使用规范化缓存的库会试图以命令式方式或依赖 schema 推断来用新数据更新本地查询;TanStack Query 的做法是避免维护规范化缓存的繁琐工作,转而提供定向失效、后台重新拉取以及原子更新的工具。
基础用法:在 Angular 组件中获取 QueryClient 并失效查询
Angular 版本通过依赖注入获取 QueryClient 实例(它由 @tanstack/angular-query-experimental 的 provider 体系注册为 DI token),然后用 injectQuery 声明查询。以下示例同时展示了「失效所有查询」和「按 key 前缀失效」两种最基本的用法:
import { inject } from '@angular/core'
import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental'
class MyComponent {
queryClient = inject(QueryClient)
// 失效缓存中的每一个查询
invalidateAll() {
this.queryClient.invalidateQueries()
}
// 失效所有 queryKey 以 `todos` 开头的查询
invalidateTodos() {
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
}
todoListQuery = injectQuery(() => ({
queryKey: ['todos'],
queryFn: fetchTodoList,
}))
}
当查询被 invalidateQueries 命中后,会发生两件事:
- 该查询被标记为过期。这个过期状态会覆盖你在
injectQuery选项中配置的staleTime; - 如果该查询当前正被
injectQuery等函数渲染(即处于 active 状态),它还会立即在后台重新拉取一次。
官方文档中给出的完整类示例(docs/framework/angular/guides/query-invalidation.md 对应内容)如下,两条查询都会被 ['todos'] 前缀命中:
import { injectQuery, QueryClient } from '@tanstack/angular-query-experimental'
class QueryInvalidationExample {
queryClient = inject(QueryClient)
invalidateQueries() {
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
}
// Both queries below will be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos'],
queryFn: fetchTodoList,
}))
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
}))
}
失效背后的源码执行链路
invalidateQueries 的实现位于 queryClient.ts,核心逻辑可以概括为三步:
invalidateQueries(
filters?: InvalidateQueryFilters,
options: InvalidateOptions = {},
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
})
if (filters?.refetchType === 'none') {
return Promise.resolve()
}
return this.refetchQueries(
{
...filters,
type: filters?.refetchType ?? filters?.type ?? 'active',
},
options,
)
})
}
三个关键点:
- 批量通知:整个流程包裹在
notifyManager.batch中,避免每次状态变更都触发一次同步渲染通知; - 标记过期:命中的每个
Query实例都会调用invalidate(),其实现见 query.ts——仅当尚未标记时派发一次invalidateaction,reducer 将状态置为isInvalidated: true(query.ts); - 默认只刷新 active 查询:随后调用
refetchQueries,type默认为'active',也就是说只有当前有订阅者正在渲染的查询会被后台拉取;如果你希望连 inactive 查询一起刷新,可以通过refetchType显式指定(见下文类型定义)。
isInvalidated 标志如何影响过期判断?见 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)
}
从源码结构看,isInvalidated 的优先级高于 staleTime 的时间计算(仅在 staleTime === 'static' 的永久新鲜场景下例外)。这意味着即使你配置了很长的 staleTime,调用 invalidateQueries 之后查询也会立刻变成 stale,进而在满足条件时重新拉取。
而实际的重新拉取发生在 queryClient.ts 的 refetchQueries 中:它对命中的查询逐一调用 query.fetch,跳过 disabled 与 static 查询,并且对 fetchStatus === 'paused'(通常由 offline 插件造成)的查询直接返回已解析的 Promise,不会发起请求。
查询匹配规则:前缀、精确与谓词
invalidateQueries 接受一个 InvalidateQueryFilters(继承自 QueryFilters),其完整定义见 types.ts:
export interface InvalidateQueryFilters<
TQueryKey extends QueryKey = QueryKey,
> extends QueryFilters<TQueryKey> {
refetchType?: QueryTypeFilter | 'none'
}
常用字段包括:
| 字段 | 说明 |
|---|---|
queryKey |
按 key 匹配;默认做前缀(部分)匹配,配合格式化 key 可命中一整族查询 |
exact |
为 true 时要求 key 完全一致(按哈希比较),不匹配子 key |
predicate |
谓词函数,接收缓存中的每个 Query 实例,返回 true 才命中,提供最高粒度 |
type |
按 active / inactive / all 过滤(invalidateQueries 默认按 active 刷新) |
refetchType |
控制刷新阶段的 type,取 QueryTypeFilter 或 'none';'none' 表示只标记过期、不触发任何刷新 |
stale / fetchStatus |
按当前过期状态或抓取状态过滤 |
更多过滤器用法可参考 Angular 侧的 Query Filters 指南。这些过滤条件最终都汇入 utils.ts 中的 matchQuery:
if (queryKey) {
if (exact) {
if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) {
return false
}
} else if (!partialMatchKey(query.queryKey, queryKey)) {
return false
}
}
// ... type / stale / fetchStatus / predicate 依次校验
1. 前缀匹配(默认行为)
不传 exact 时,invalidateQueries 使用 partialMatchKey 做前缀匹配。它的规则实现在 utils.ts:数组按索引逐项比较(过滤 key 比查询 key 短时视为前缀匹配),对象只要求过滤对象中出现的字段在查询 key 中逐一相等,因此对象字段顺序不同也不影响命中。
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 以下两个查询都会被失效
todoListQuery = injectQuery(() => ({
queryKey: ['todos'],
queryFn: fetchTodoList,
}))
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
}))
2. 带更具体变量的匹配
把更具体的 queryKey 传给 invalidateQueries,可以只命中携带该变量的查询:
queryClient.invalidateQueries({
queryKey: ['todos', { type: 'done' }],
})
// The query below will be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
}))
// However, the following query below will NOT be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos'],
queryFn: fetchTodoList,
}))
注意方向性:过滤 key 必须是查询 key 的“前缀子集”。['todos', { type: 'done' }] 不能命中更“短”的 ['todos'],这与上面前缀匹配的方向一致——匹配是查询 key 包含过滤 key,而不是反过来。
3. exact:只命中没有子 key 的查询
如果你只想失效不带任何额外变量或子 key 的 todos 查询,传 exact: true:
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true,
})
// The query below will be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos'],
queryFn: fetchTodoList,
}))
// However, the following query below will NOT be invalidated
const todoListQuery = injectQuery(() => ({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
}))
从源码看,exact 模式下不走 partialMatchKey,而是直接比较 query.queryHash 与 hashQueryKeyByOptions(queryKey, query.options)(utils.ts),即按 key 的哈希做全等判断,天然排除了带子 key 的查询。
4. predicate:最高粒度的谓词匹配
当你需要比 key 结构更复杂的条件(比如按 key 中某个数值字段的大小过滤),可以传入 predicate 函数。它会接收缓存中的每个 Query 实例,返回 true 才失效该查询:
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
// The query below will be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { version: 20 }],
queryFn: fetchTodoList,
}))
// The query below will be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { version: 10 }],
queryFn: fetchTodoList,
}))
// However, the following query below will NOT be invalidated
todoListQuery = injectQuery(() => ({
queryKey: ['todos', { version: 5 }],
queryFn: fetchTodoList,
}))
在 utils.ts 中,predicate 是 matchQuery 的最后一道校验:if (predicate && !predicate(query)) return false,因此它可以与 queryKey、exact、type 等条件叠加使用。
5. refetchType:只标记过期而不刷新
默认情况下 invalidateQueries 会对命中的 active 查询发起后台刷新;若你只希望“标记为过期”(例如延迟到用户回到页面时再拉取),可以传 refetchType: 'none'。从 queryClient.ts 看,此时方法在标记完成后直接 return Promise.resolve(),跳过 refetchQueries 阶段。该 Promise 的解析时机与底层刷新行为(取消/并发策略)由 RefetchOptions(如 cancelRefetch、throwOnError)控制,可在 types.ts 中查看默认值说明。
Angular 侧的获取方式与注意事项
- 推荐通过 DI 获取:文档示例统一使用
inject(QueryClient)。仓库中保留的injectQueryClient辅助函数已标记为废弃(见 inject-query-client.ts),其 JSDoc 明确建议改用inject(QueryClient),需要自定义 injector 时用injector.get(QueryClient); - 查询声明用
injectQuery:Angular 版以“函数”(inject function)取代 React 版的 hooks,选项以惰性函数传入,便于在信号(signal)上下文中响应式地依赖其他信号值构造 key; - 失效是幂等且批量的:
invalidate()内部会先检查isInvalidated避免重复派发,而invalidateQueries全程运行在notifyManager.batch中,对含大量查询的应用来说,这意味着一次失效操作只会触发一次批量通知; - 配合官方示例理解上下文:仓库中 examples/angular/optimistic-updates 与 examples/angular/auto-refetching 等示例工程展示了失效与变更、自动刷新组合使用的完整可运行工程,适合作为上述机制的对照阅读。
小结
invalidateQueries 是 TanStack Query 中“定向失效”的核心 API:它先用 queryKey(前缀或 exact 精确)与 predicate 谓词在缓存中筛选查询,再统一打上 isInvalidated 标记(覆盖 staleTime),最后默认对 active 查询发起一次后台刷新(可用 refetchType: 'none' 关闭刷新)。在 Angular 应用中,你只需通过 inject(QueryClient) 拿到客户端实例,即可在组件或服务的任意生命周期钩子(如变更操作成功后)调用它,完成精确到单个变量、甚至按业务谓词的缓存失效与数据同步。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00