首页
/ TanStack Query for Angular(experimental):injectMutation 函数深度解析

TanStack Query for Angular(experimental):injectMutation 函数深度解析

2026-09-06 18:32:56作者:羿妍玫Ivan

本文基于 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 侧的 dataerror、状态收窄谓词则据此推断,从而获得端到端的类型安全。

参数说明

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-coreMutationObserverOptions 的镜像(去掉内部字段 _defaulted),因此 mutationFnmutationKeyonMutateonSuccessonErroronSettledretrythrowOnError 等所有核心 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),核心特征是:

  • 所有结果字段(dataerrorstatusstate 等)都映射为 signal,模板与代码中通过 mutation.data() 读取;
  • isIdle / isPending / isError / isSuccessSignalFunction:既可作为 mutation.isPending() 这样的 signal 调用,又可作为 TypeScript 类型谓词参与状态收窄(见 BaseMutationNarrowingtypes.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: truemutationFn resolve 后 isSuccess: truedata() 返回结果;若 mutationFn reject,则 isError: trueerror()?.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 参数的意义:如果不传 injectorinjectMutation 必须运行在注入上下文中(组件/服务的字段初始化器、runInInjectionContext 回调等),否则 assertInInjectionContext 直接抛错。随后从 Injector 解析出 NgZonePENDING_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 })

这段订阅链体现了三组工程决策:

  1. Zone 边界:订阅注册在 ngZone.runOutsideAngular 中,每次状态更新再进入 ngZone.run 写信号——mutation 回调更新 UI 时能正确触发变更检测,而高频的中间通知不会无谓地把 Zone 拉回 Angular 一侧;通知本身还经过 notifyManager.batchCalls 批量合并,减少渲染次数。
  2. pending task 追踪:mutation 进入 pending 时通过 pendingTasks.add() 登记,settled 或组件销毁时释放。这是 Angular 版针对 lazy hydration / 应用启动期间未完成异步任务场景的配套机制,由 PENDING_TASKSpending-tasks-compat.ts 提供)实现,专门有 pending-tasks.test.ts 覆盖。
  3. throwOnError 策略:错误状态下是否向外抛错,由 shouldThrowError(observer.options.throwOnError, [state.error]) 判定——这与 query-core 的语义一致,throwOnErrortruefalse 或谓词函数都受支持;命中时先 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<...>

signalProxysignal-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/ 中:

需要注意的前提与限制:该模块位于 @tanstack/angular-query-experimental 实验性包中,API 可能随版本演进;使用 injectMutation 必须先通过 provideTanStackQueryprovideQueryClient 注册 QueryClient,且调用需处于注入上下文(除非显式传入 options.injector)。更广泛的框架级说明可参考 Angular 概览快速上手

七、要点总结

  1. injectMutation 返回一个"属性即 signal"的 mutation 对象,mutate 发射即忘、mutateAsync 可 await,状态通过 isIdle / isPending / isError / isSuccessdata() / error() 信号读取;
  2. 第一个参数必须是函数,这是响应式 options 的前提:函数体内的信号依赖会被 computed 追踪并自动重算同步给底层 MutationObserver
  3. 默认要求在注入上下文中调用;options.injector 可覆盖默认的 Injector 解析;
  4. 底层订阅运行在 runOutsideAngular + batchCalls 的优化路径上,更新写回信号时回到 ngZone.run,并按 throwOnError 配置决定是否经 ngZone.onError 抛出错误;
  5. mutation 处于 pending 期间的生命周期由 PENDING_TASKS 配套机制登记与清理,保证销毁时不泄漏。
登录后查看全文
热门项目推荐
相关项目推荐