首页
/ TanStack Query(Angular)查询失效实战:用 invalidateQueries 精确标记过期数据并触发后台刷新

TanStack Query(Angular)查询失效实战:用 invalidateQueries 精确标记过期数据并触发后台刷新

2026-09-06 14:39:45作者:范垣楠Rhoda

本篇围绕 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 命中后,会发生两件事:

  1. 该查询被标记为过期。这个过期状态会覆盖你在 injectQuery 选项中配置的 staleTime
  2. 如果该查询当前正被 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——仅当尚未标记时派发一次 invalidate action,reducer 将状态置为 isInvalidated: truequery.ts);
  • 默认只刷新 active 查询:随后调用 refetchQueriestype 默认为 '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.tsrefetchQueries 中:它对命中的查询逐一调用 query.fetch,跳过 disabledstatic 查询,并且对 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 的查询

如果你只想失效不带任何额外变量或子 keytodos 查询,传 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.queryHashhashQueryKeyByOptions(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 中,predicatematchQuery 的最后一道校验:if (predicate && !predicate(query)) return false,因此它可以与 queryKeyexacttype 等条件叠加使用。

5. refetchType:只标记过期而不刷新

默认情况下 invalidateQueries 会对命中的 active 查询发起后台刷新;若你只希望“标记为过期”(例如延迟到用户回到页面时再拉取),可以传 refetchType: 'none'。从 queryClient.ts 看,此时方法在标记完成后直接 return Promise.resolve(),跳过 refetchQueries 阶段。该 Promise 的解析时机与底层刷新行为(取消/并发策略)由 RefetchOptions(如 cancelRefetchthrowOnError)控制,可在 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-updatesexamples/angular/auto-refetching 等示例工程展示了失效与变更、自动刷新组合使用的完整可运行工程,适合作为上述机制的对照阅读。

小结

invalidateQueries 是 TanStack Query 中“定向失效”的核心 API:它先用 queryKey(前缀或 exact 精确)与 predicate 谓词在缓存中筛选查询,再统一打上 isInvalidated 标记(覆盖 staleTime),最后默认对 active 查询发起一次后台刷新(可用 refetchType: 'none' 关闭刷新)。在 Angular 应用中,你只需通过 inject(QueryClient) 拿到客户端实例,即可在组件或服务的任意生命周期钩子(如变更操作成功后)调用它,完成精确到单个变量、甚至按业务谓词的缓存失效与数据同步。

登录后查看全文
热门项目推荐
相关项目推荐