TanStack Query for Angular(experimental):injectMutation 函数深度解析
本文基于 Angular 参考文档中的 injectMutation 条目,结合 @tanstack/angular-query-experimental 包的真实源码与测试,完整讲解这一 API 的函数签名、类型参数、选项接口与返回结构,并深入剖析其底层的信号(Signals)实现原理——包括响应式 options、NgZone 集成、错误处理策略与 pending task 追踪。读完本文,你将掌握在 Angular 注入上下文中以信号方式创建、共享与观察 mutation 的完整实战方案。
一、injectMutation 是什么
injectMutation 定义在 inject-mutation.ts 中,并通过 index.ts 导出,属于 @tanstack/angular-query-experimental 包(当前仓库版本为 5.102.8,见 package.json)。官方文档对它的定义非常简洁:
Injects a mutation: an imperative function that can be invoked which typically performs server side effects. Unlike queries, mutations are not run automatically.
翻译过来即:注入一个 mutation——一个可以被显式调用的命令式函数,通常用于执行服务端副作用。与 query 不同,mutation 不会自动运行。 这一点与 TanStack Query 各框架版的一致性约定相同:只有 query 会随组件挂载/依赖变化自动发起请求,而 mutation(增删改操作)必须由代码主动触发。
与 injectQuery 等 API 一样,injectMutation 采用"传入一个函数"的注入模式(注意第一个参数名就叫 injectMutationFn),这是整个 Angular 版 API 的统一风格:传入 () => options 而非 options 对象本身。
二、函数签名与类型参数
参考文档 injectMutation.md 给出的完整签名:
function injectMutation<TData, TError, TVariables, TOnMutateResult>(
injectMutationFn,
options?
): CreateMutationResult<TData, TError, TVariables, TOnMutateResult>;
与源码 inject-mutation.ts#L45-L58 对照,带默认值的完整签名是:
export function injectMutation<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
>(
injectMutationFn: () => CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
options?: InjectMutationOptions,
): CreateMutationResult<TData, TError, TVariables, TOnMutateResult>
类型参数
| 类型参数 | 默认值 | 含义 |
|---|---|---|
TData |
unknown |
mutationFn 成功后 resolve 的数据类型 |
TError |
DefaultError(即 Error) |
失败时的错误类型 |
TVariables |
void |
调用 mutate / mutateAsync 时传入的变量类型;为 void 时可无参调用 |
TOnMutateResult |
unknown |
onMutate 回调的返回值类型,决定了乐观更新中 context 的类型 |
这四个泛型参数贯穿 options 与 result 两个方向:options 侧的 mutationFn: (vars: TVariables) => Promise<TData> 决定输入输出类型,result 侧的 data、error、状态收窄谓词则据此推断,从而获得端到端的类型安全。
参数说明
1. injectMutationFn
类型是 () => CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,"一个返回 mutation options 的函数"。其中 CreateMutationOptions 定义于 types.ts#L126-L134:
export interface CreateMutationOptions<
TData = unknown,
TError = DefaultError,
TVariables = void,
TOnMutateResult = unknown,
> extends OmitKeyof<MutationObserverOptions<TData, TError, TVariables, TOnMutateResult>, '_defaulted'> {}
也就是说,它本质上是 query-core 中 MutationObserverOptions 的镜像(去掉内部字段 _defaulted),因此 mutationFn、mutationKey、onMutate、onSuccess、onError、onSettled、retry、throwOnError 等所有核心 mutation 配置项均可直接使用,参考 CreateMutationOptions 接口文档。
这里有一个关键设计:options 必须放在函数体内,因为源码会把它包进 computed()(见下文原理部分),使 options 具备信号响应性——函数体内引用的 signal 变化时,observer 的选项会自动重新求值并同步。
2. options?(可选)
类型为 InjectMutationOptions,源码中只有一个字段:
export interface InjectMutationOptions {
/**
* The `Injector` in which to create the mutation.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
即可以显式指定 mutation 所依附的 Injector;不提供时,使用当前注入上下文(内部通过 inject(Injector) 获取)。
3. 返回值
返回 CreateMutationResult。它是 MutationObserverResult 的 Angular 信号化版本(定义于 types.ts#L260-L273),核心特征是:
- 所有结果字段(
data、error、status、state等)都映射为 signal,模板与代码中通过mutation.data()读取; isIdle/isPending/isError/isSuccess是 SignalFunction:既可作为mutation.isPending()这样的 signal 调用,又可作为 TypeScript 类型谓词参与状态收窄(见BaseMutationNarrowing,types.ts#L184-L258);mutate是返回void的"发射即忘"版本(CreateMutateFunction),而mutateAsync保留 promise 语义(CreateMutateAsyncFunction),便于await后做后续逻辑。
三、最小可用示例与状态流转
以下示例与官方测试 inject-mutation.test.ts 的用法一致(测试中使用 TestBed + provideTanStackQuery(queryClient) 提供 QueryClient):
@Component({
selector: 'app-page',
template: `
<div>isIdle: {{ mutation.isIdle() }}</div>
<div>isPending: {{ mutation.isPending() }}</div>
<div>isError: {{ mutation.isError() }}</div>
<div>isSuccess: {{ mutation.isSuccess() }}</div>
<div data-test="data">{{ mutation.data() ?? 'none' }}</div>
<div data-test="error">{{ mutation.error()?.message ?? 'none' }}</div>
<button (click)="mutation.mutate('Mock data')">Save</button>
`,
})
export class Page {
// 在注入上下文(组件/服务字段初始化器)中创建 mutation
readonly mutation = injectMutation(() => ({
mutationFn: (params: string) => sleep(10).then(() => params),
}))
}
测试用例验证了完整的状态机:初始为 isIdle: true 且其余三个标志位为 false;调用 mutation.mutate('Mock data') 后进入 isPending: true;mutationFn resolve 后 isSuccess: true 且 data() 返回结果;若 mutationFn reject,则 isError: true 且 error()?.message 可读。这些断言分别位于 inject-mutation.test.ts#L35-L48(初始 idle 态)、#L50-L81(pending 态)与 #L83-L114(error 态)。
四、源码级实现剖析
阅读 inject-mutation.ts,整个函数可以拆解为五条信号流水线。
4.1 注入上下文断言与依赖解析
!options?.injector && assertInInjectionContext(injectMutation)
const injector = options?.injector ?? inject(Injector)
const ngZone = injector.get(NgZone)
const pendingTasks = injector.get(PENDING_TASKS)
const queryClient = injector.get(QueryClient)
这解释了文档中 options.injector 参数的意义:如果不传 injector,injectMutation 必须运行在注入上下文中(组件/服务的字段初始化器、runInInjectionContext 回调等),否则 assertInInjectionContext 直接抛错。随后从 Injector 解析出 NgZone、PENDING_TASKS(pending 任务追踪器)与 QueryClient 三个协作依赖。
4.2 options 信号化:响应式配置的核心
/**
* computed() is used so signals can be inserted into the options
* making it reactive. Wrapping options in a function ensures embedded expressions
* are preserved and can keep being applied after signal changes
*/
const optionsSignal = computed(injectMutationFn)
这段源码注释直接回答了"为什么传函数而不是对象":把用户函数包进 computed(),函数体内读取的任何 signal 都会被追踪;信号变化时 optionsSignal() 重新执行,且每次都是重新调用用户函数,因此内嵌表达式(如依赖 id() 计算的 mutationKey)会持续保持最新。紧接着:
const observerSignal = (() => {
let instance: MutationObserver<TData, TError, TVariables, TOnMutateResult> | null = null
return computed(() => {
return (instance ||= new MutationObserver(queryClient, optionsSignal()))
})
})()
MutationObserver(来自 @tanstack/query-core)只实例化一次,之后由 effect 负责把新的 options 同步给它(4.4 节)。
4.3 mutate 函数信号
const mutateFnSignal = computed(() => {
const observer = observerSignal()
return (...args) => {
observer.mutate(args[0] as TVariables, args[1]).catch(noop)
}
})
mutate 内部调用 observer.mutate(...) 后 .catch(noop) 吞掉 promise 错误——这就是"mutate 永不 reject,错误只体现在 error() signal 上"这一行为的来源;需要异常语义时应使用 mutateAsync(最终结果中 mutateAsync: result.mutate,即保留原始 promise 行为,见 inject-mutation.ts#L180-L191)。
4.4 两个 effect:同步 options 与订阅结果
第一个 effect 在 options 变化时把最新配置推给 observer:
effect(() => {
const observer = observerSignal()
const observerOptions = optionsSignal()
untracked(() => { observer.setOptions(observerOptions) })
}, { injector })
第二个 effect 是订阅与错误处理的核心,完整逻辑在 inject-mutation.ts#L130-L178:
effect((onCleanup) => {
const observer = observerSignal()
let pendingTaskRef: PendingTaskRef | null = null
untracked(() => {
const unsubscribe = ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
ngZone.run(() => {
if (state.isPending && !pendingTaskRef) {
pendingTaskRef = pendingTasks.add()
}
if (!state.isPending && pendingTaskRef) {
pendingTaskRef()
pendingTaskRef = null
}
if (state.isError && shouldThrowError(observer.options.throwOnError, [state.error])) {
ngZone.onError.emit(state.error)
throw state.error
}
resultFromSubscriberSignal.set(state)
})
}),
),
)
onCleanup(() => {
if (pendingTaskRef) { pendingTaskRef(); pendingTaskRef = null }
unsubscribe()
})
})
}, { injector })
这段订阅链体现了三组工程决策:
- Zone 边界:订阅注册在
ngZone.runOutsideAngular中,每次状态更新再进入ngZone.run写信号——mutation 回调更新 UI 时能正确触发变更检测,而高频的中间通知不会无谓地把 Zone 拉回 Angular 一侧;通知本身还经过notifyManager.batchCalls批量合并,减少渲染次数。 - pending task 追踪:mutation 进入 pending 时通过
pendingTasks.add()登记,settled 或组件销毁时释放。这是 Angular 版针对 lazy hydration / 应用启动期间未完成异步任务场景的配套机制,由PENDING_TASKS(pending-tasks-compat.ts 提供)实现,专门有 pending-tasks.test.ts 覆盖。 - throwOnError 策略:错误状态下是否向外抛错,由
shouldThrowError(observer.options.throwOnError, [state.error])判定——这与 query-core 的语义一致,throwOnError为true、false或谓词函数都受支持;命中时先ngZone.onError.emit(state.error)再抛出。若使用默认throwOnError配置,mutate()路径下错误只会写入error()signal,不会让未捕获异常逃逸到 Zone 之外。
最后,resultSignal 合并两条结果来源:订阅尚未触发前取 observer.getCurrentResult()(resultFromInitialOptionsSignal),订阅写入后以 resultFromSubscriberSignal 为准,再附上 mutate / mutateAsync:
const resultSignal = computed(() => {
const resultFromSubscriber = resultFromSubscriberSignal()
const resultFromInitialOptions = resultFromInitialOptionsSignal()
const result = resultFromSubscriber ?? resultFromInitialOptions
return { ...result, mutate: mutateFnSignal(), mutateAsync: result.mutate }
})
return signalProxy(resultSignal) as CreateMutationResult<...>
signalProxy(signal-proxy.ts)把整个 computed 包装成可像普通对象一样按属性读取的代理,于是使用者得到 mutation.data()、mutation.isPending() 这类"属性即信号"的 API。
五、配合 mutationOptions 复用配置
参考文档中 injectMutationFn 的类型指向 CreateMutationOptions,而仓库专门提供 mutationOptions 帮助函数来在组件/服务之间类型安全地共享 mutation 配置(实现见 mutation-options.ts,带 mutationKey 的重载强制要求 key 必传)。官方文档示例:
export class QueriesService {
private http = inject(HttpClient)
private queryClient = inject(QueryClient)
updatePost(id: number) {
return mutationOptions({
mutationFn: (post: Post) => Promise.resolve(post),
mutationKey: ['updatePost', id],
onSuccess: (newPost) => {
// ^? newPost: Post
this.queryClient.setQueryData(['posts', id], newPost)
},
})
}
}
class ComponentOrService {
queries = inject(QueriesService)
id = signal(0)
mutation = injectMutation(() => this.queries.updatePost(this.id()))
save() {
this.mutation.mutate({ title: 'New Title' })
}
}
这个示例恰好演示了 4.2 节的响应式机制:this.id() 是 signal,包在 injectMutationFn 里后,id 一变,mutationKey 与整个 observer 配置都会自动更新——这正是"options 必须是函数"这一约束的实战收益。
六、相关 API 与延伸阅读
injectMutation 并非孤立存在,Angular 版围绕 mutation 还提供了一组配套函数,均在 docs/framework/angular/reference/functions/ 中:
- injectMutationState:注入一个
Signal<MutationState[]>,跨组件追踪所有 mutation 的状态(例如全局"是否有请求在途"的指示器),实现见 inject-mutation-state.ts; - injectIsMutating:判断是否存在处于 mutating 状态的 mutation;
- provideTanStackQuery / provideQueryClient:提供
injectMutation所依赖的QueryClient; - mutationOptions:如上所述的配置共享工具。
需要注意的前提与限制:该模块位于 @tanstack/angular-query-experimental 实验性包中,API 可能随版本演进;使用 injectMutation 必须先通过 provideTanStackQuery 或 provideQueryClient 注册 QueryClient,且调用需处于注入上下文(除非显式传入 options.injector)。更广泛的框架级说明可参考 Angular 概览 与 快速上手。
七、要点总结
injectMutation返回一个"属性即 signal"的 mutation 对象,mutate发射即忘、mutateAsync可 await,状态通过isIdle/isPending/isError/isSuccess与data()/error()信号读取;- 第一个参数必须是函数,这是响应式 options 的前提:函数体内的信号依赖会被
computed追踪并自动重算同步给底层MutationObserver; - 默认要求在注入上下文中调用;
options.injector可覆盖默认的 Injector 解析; - 底层订阅运行在
runOutsideAngular+batchCalls的优化路径上,更新写回信号时回到ngZone.run,并按throwOnError配置决定是否经ngZone.onError抛出错误; - mutation 处于 pending 期间的生命周期由
PENDING_TASKS配套机制登记与清理,保证销毁时不泄漏。
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 StartedRust0624
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