TanStack Query Angular:用 injectIsMutating 信号追踪全局变更(Mutation)加载状态
本文围绕 TanStack Query 的 Angular 适配器包 @tanstack/angular-query-experimental 中的 injectIsMutating 函数展开:它如何以 Angular Signal 的形式返回“当前正在执行中的 mutation 数量”,参数如何配置,以及底层如何订阅 MutationCache、配合 NgZone 与 notifyManager 实现响应式更新。读完本文,你可以掌握在 Angular 应用中构建全局或按 mutationKey 过滤的“提交中”加载指示器,并理解该 API 的注入上下文约束与源码级实现原理。
一、API 概览
injectIsMutating 定义于 inject-is-mutating.ts 中,其签名如下:
function injectIsMutating(filters?, options?): Signal<number>;
它的职责是:注入一个信号(Signal),追踪你的应用中正在发起(fetching/pending)的 mutation 数量。典型用途是构建应用级的加载指示器(app-wide loading indicators)——例如当有任意表单提交、删除操作正在进行时,在页面顶部显示一条“正在提交…”的提示条。
需要注意的适用前提:该 API 属于 @tanstack/angular-query-experimental 包(对应仓库目录 packages/angular-query-experimental),即 Angular 适配器当前处于实验(experimental)阶段。它与同族的 injectIsFetching(追踪 query 的 fetch 数量)、injectIsRestoring(追踪持久化恢复状态)构成一组“全局状态信号”API。
二、参数详解
2.1 filters? — 过滤条件
filters?: MutationFilters<unknown, Error, unknown, unknown>
应用于 mutation 的过滤条件,类型来自 @tanstack/query-core 导出的 MutationFilters。最常见的用法是按 mutationKey 过滤,只统计某一类变更操作。例如:
// 只统计 mutationKey 为 ['deleteTodo'] 的 mutation 数量
readonly deleteMutating = injectIsMutating({ mutationKey: ['deleteTodo'] })
从源码结构看,这些过滤条件最终会透传给 MutationCache 的 findAll 方法——在 mutationCache.ts 中可以看到 findAll(filters: MutationFilters = {}): Array<Mutation> 负责按条件筛选出匹配的 mutation 实例。
2.2 options? — 额外配置
options?: InjectIsMutatingOptions
对应接口定义在 InjectIsMutatingOptions.md,源码见 inject-is-mutating.ts#L13-L20:
export interface InjectIsMutatingOptions {
/**
* The `Injector` in which to create the isMutating signal.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
唯一的可选属性 injector 用于指定创建该信号所挂靠的 Injector。如果不提供,函数会通过 inject(Injector) 使用当前注入上下文——这要求调用点必须处于合法的注入上下文中(组件/服务构造函数、字段初始化器、runInInjectionContext 回调等),否则 Angular 会抛出 NG0203 错误。显式传入 injector 则是唯一允许脱离注入上下文调用该 API 的方式。
2.3 返回值
Signal<number> —— 一个只读信号,值为当前处于 pending 状态的 mutation 数量。调用 isMutating() 即可在模板或逻辑中读取最新值,信号变化会自动驱动 Angular 变更检测。
三、实战用法
3.1 应用级“提交中”指示器
下面的示例展示了 injectIsMutating 的典型用法:在根组件中注入信号,当计数大于 0 时展示全局提示:
import { Component } from '@angular/core'
import { injectIsMutating } from '@tanstack/angular-query-experimental'
@Component({
selector: 'app-global-busy-indicator',
template: `
@if (mutatingCount() > 0) {
<div class="global-busy">正在处理 {{ mutatingCount() }} 个请求…</div>
}
`,
})
export class GlobalBusyIndicatorComponent {
// 字段初始化器中调用,处于注入上下文,合法
readonly mutatingCount = injectIsMutating()
}
其思想与 Background Fetching Indicators 指南中用 injectIsFetching 构建全局 query 加载指示器的方式完全对称——一个面向 query 读取,一个面向 mutation 写入。
3.2 按 mutationKey 过滤
当你只关心某类操作(如“删除”)的进行状态时,传入 MutationFilters:
import { injectIsMutating, injectMutation } from '@tanstack/angular-query-experimental'
@Component({
selector: 'todos',
template: `
<button [disabled]="deleting()">删除</button>
`,
})
export class TodosComponent {
// 按 mutationKey 过滤:仅统计 ['deleteTodo'] 类 mutation
readonly deleting = injectIsMutating({ mutationKey: ['deleteTodo'] })
readonly deleteMutation = injectMutation(() => ({
mutationKey: ['deleteTodo'],
mutationFn: (id: number) => deleteTodo(id),
}))
}
仓库中的单元测试 inject-is-mutating.test.ts 验证了该过滤行为:同时发起两个不同 mutationKey 的 mutation 后,按 key1 过滤的信号只计到 1,key1 对应的 mutation 完成后回落到 0,不受 key2 慢请求的影响。
3.3 测试环境中的调用
在 TestBed 单测中,若不在注入上下文里(例如普通 it 回调中),需要包一层 runInInjectionContext:
const [mutation, isMutating] = TestBed.runInInjectionContext(() => [
injectMutation(() => mutationOptions),
injectIsMutating(),
])
这是 mutation-options.test.ts 中实际采用的写法。
四、源码级实现解析
injectIsMutating 的完整实现非常紧凑,位于 inject-is-mutating.ts#L30-L64。逐段拆解:
export function injectIsMutating(
filters?: MutationFilters,
options?: InjectIsMutatingOptions,
): Signal<number> {
!options?.injector && assertInInjectionContext(injectIsMutating)
const injector = options?.injector ?? inject(Injector)
const destroyRef = injector.get(DestroyRef)
const ngZone = injector.get(NgZone)
const queryClient = injector.get(QueryClient)
const cache = queryClient.getMutationCache()
// isMutating is the prev value initialized on mount *
let isMutating = queryClient.isMutating(filters)
const result = signal(isMutating)
const unsubscribe = ngZone.runOutsideAngular(() =>
cache.subscribe(
notifyManager.batchCalls(() => {
const newIsMutating = queryClient.isMutating(filters)
if (isMutating !== newIsMutating) {
// * and update with each change
isMutating = newIsMutating
ngZone.run(() => {
result.set(isMutating)
})
}
}),
),
)
destroyRef.onDestroy(unsubscribe)
return result.asReadonly()
}
可以提炼出五个关键设计点:
-
注入上下文守卫:
!options?.injector && assertInInjectionContext(injectIsMutating)—— 只有在未显式传入injector时才要求处于注入上下文。测试用例 inject-is-mutating.test.ts#L98-L112 验证了两种边界:脱离上下文直接调用会抛出包含NG0203与injectIsMutating描述的错误;传入injector后则可以在任意位置安全调用。 -
初始值来自当前缓存状态:信号初始值不是固定的
0,而是queryClient.isMutating(filters)的即时计算结果。这意味着即使组件在“已有 mutation 正在执行”时才挂载,信号初始值也会正确反映真实数量(源码注释isMutating is the prev value initialized on mount即此意)。 -
在 Angular 变更检测之外订阅缓存:
cache.subscribe被包裹在ngZone.runOutsideAngular中——mutation 状态变更本身不触发 Angular 变更检测,避免不必要的渲染开销;只有当计数值真正发生变化时,才通过ngZone.run(() => result.set(isMutating))回到 Zone 内更新信号,由信号机制精准驱动 UI 刷新。 -
notifyManager.batchCalls批处理:订阅回调经过notifyManager.batchCalls包裹(定义于 notifyManager.ts)。当一次操作引发多个 mutation 状态变更(如并发提交多个请求)时,通知会被批处理合并,信号只需一次重算与一次更新。 -
生命周期自动清理:
destroyRef.onDestroy(unsubscribe)把取消订阅绑定到宿主注入器(DestroyRef取自injector.get(DestroyRef))的销毁周期。组件销毁后,对MutationCache的订阅自动移除,不会泄漏。返回的result.asReadonly()则保证外部拿到的只能是只读信号,无法绕过组件直接set污染内部状态。
4.1 计数逻辑到底数什么
queryClient.isMutating(filters) 是核心计数入口,其实现位于 queryClient.ts#L116-L120:
isMutating<
TMutationFilters extends MutationFilters<any, any> = MutationFilters,
>(filters?: TMutationFilters): number {
return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length
}
也就是说:它返回的是缓存中所有满足你传入的 filters 且状态为 pending(已发起、尚未成功/失败)的 mutation 数量。与之对称,queryClient.isFetching 返回的是 fetchStatus: 'fetching' 的 query 数量(queryClient.ts#L109-L114)。
这里有一个值得注意的区别:queryClient.isMutating 是命令式(非响应)的一次性快照,适合在回调中读取;injectIsMutating 是在其之上构建的响应式版本——把“快照 + 订阅变更”封装成了生命周期安全的 Signal。仓库的类型测试 mutation-options.test-d.ts#L179-L207 也确认了两者的类型一致性:injectIsMutating() 的读取结果与 queryClient.isMutating(...) 一致,均为 number。
五、测试用例中的行为验证
单元测试 inject-is-mutating.test.ts 在 provideZonelessChangeDetection() + provideTanStackQuery(queryClient) 的 TestBed 配置下,验证了以下行为链路(测试用例 L36-L63):
| 阶段 | 操作 | 模板渲染结果 |
|---|---|---|
| 初始 | 组件渲染,无进行中 mutation | mutating: 0 |
| 发起 | mutation.mutate({ par1: 'par1' }) 后推进 fake timer |
mutating: 1 |
| 完成 | mutationFn(sleep(10))解析完成后 |
mutating: 0 |
该测试同时印证了与 injectMutation 的协作:mutation 的 pending/成功/失败状态变化会实时反映到 injectIsMutating 信号中。多 mutation 并发的计数行为(1 个、2 个、按 key 过滤后各计 1 个)则分别在 mutation-options.test.ts#L78-L90 与 #L103-L119 中有对应的断言覆盖。
六、选型建议与注意事项
- 要“响应式信号”还是“一次性读数”:在模板/信号驱动的组件内使用,选
injectIsMutating;在事件回调、命令式代码中临时取数,直接用queryClient.isMutating(filters)即可,无需建立订阅。 - 必须处于注入上下文:忘记传
injector又在非注入上下文调用,会直接抛NG0203。测试代码中TestBed.runInInjectionContext的包裹方式是标准解法。 - 信号是只读的:返回
asReadonly()后的信号不接受外部set,这与 injectIsFetching.md 等同类 API 的行为一致。 - 计数语义是“pending”:只有处于 pending 状态的 mutation 会被计入;已失败或成功的 mutation 立即不再计数。如果你的 UI 需要“失败后仍短暂提示”,应结合
injectMutation自身的状态或MutationCache的其他订阅方式处理。 - 过滤透传:
filters与queryClient.isMutating使用同一套MutationFilters,二者对同一缓存的计数结果在任何时刻都保持一致,可以互相交叉验证。
综上,injectIsMutating 是 TanStack Query Angular 适配器中把“mutation 并发状态”响应式地暴露给模板层的核心工具:它以内联的 Injector/DestroyRef/NgZone 三件套保证生命周期与变更检测的正确性,以 MutationCache 订阅 + batchCalls 批处理保证更新的及时性与高效性,是构建全局提交指示器、并发操作计数器等 UI 场景的首选 API。
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 StartedRust0623
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