首页
/ Angular Query 中 provideIsRestoring 解析:信号化持久化恢复状态,避免查询与数据恢复之间的竞态

Angular Query 中 provideIsRestoring 解析:信号化持久化恢复状态,避免查询与数据恢复之间的竞态

2026-09-07 09:19:49作者:卓艾滢Kingsley

Angular Query(TanStack Query Angular 适配,仓库中位于 packages/angular-query-experimental)通过 provideIsRestoring 将"是否正在恢复持久化查询数据"的状态以 Angular Signal 的形式注入依赖注入容器,供 injectQueryinjectQueries 等 API 内部消费,从而在数据恢复期间暂停查询订阅、按"恢复中"模式渲染乐观结果。读完本文,你将掌握 provideIsRestoring 的函数签名、底层 InjectionToken 机制、它如何与持久化插件 withPersistQueryClient 配合工作,以及它和配套读取函数 injectIsRestoring 的关系。

函数概览与适用场景

provideIsRestoring 是一个纯框架层的辅助 Provider 工厂函数,定义在 inject-is-restoring.ts

function provideIsRestoring(isRestoring): Provider;

它的职责非常单一:接收一个表示"恢复状态"的只读信号(readonly signal),把它绑定到模块级私有的注入令牌上,返回一个可供 Angular DI 使用的 Provider。原始文档也明确指出其服务对象:

Used by TanStack Query Angular persist client plugin to provide the signal that tracks the restore state

即它主要由持久化客户端插件(@tanstack/angular-query-persist-clientwithPersistQueryClient)在内部调用,普通应用开发者通常不需要直接编写业务代码去使用它;当你想自定义一套"恢复期间的状态展示"逻辑时,它才成为开放的扩展点。

该函数与配套的读取函数 injectIsRestoring 一起,在 index.ts 中被作为公共 API 导出:

export { injectIsRestoring, provideIsRestoring } from './inject-is-restoring'

参数说明

isRestoring

Signal<boolean> 类型,要求传入一个只读信号(readonly signal),该信号返回布尔值,表示"当前是否正处于持久化数据的恢复过程中"。传入时通常应把可写的信号通过 .asReadonly() 转成只读视图后传入(见下文 withPersistQueryClient 的做法)。

返回值

Provider —— 一个将传入信号与内部注入令牌 IS_RESTORING 绑定的 Angular Provider,可被放入组件、指令的 providers,或通过 ApplicationConfig.providers 注册到应用级。

底层实现:InjectionToken + useValue

provideIsRestoring 的实现非常精简,本质上就是一次"令牌到值"的绑定:

const IS_RESTORING = new InjectionToken('', {
  // Default value when not provided
  factory: () => signal(false).asReadonly(),
})

export function provideIsRestoring(isRestoring: Signal<boolean>): Provider {
  return {
    provide: IS_RESTORING,
    useValue: isRestoring,
  }
}

两点关键设计值得注意:

  1. 默认值为 false:注入令牌在未调用 provideIsRestoring 时,通过 factory 提供一个恒为 false 的只读信号。这意味着不启用持久化功能的普通应用中,isRestoring 永远是 false,查询走常规的乐观更新路径,行为与旧版无差异。
  2. useValue 直接引用传入信号:Provider 不复制信号、也不包装一层新信号,而是把外部传入的信号对象本身作为令牌的值。因此外部信号后续 set / update 引起的变化会响应式地传导到所有 injectIsRestoring() 的读取方——这正是测试 should reactively reflect changes to the provided signal 所验证的行为。

需要说明的是,IS_RESTORING 令牌本身是模块私有常量(源码注释标注为 "Internal token"),外部无法直接注入该令牌,只能通过公共 API provideIsRestoring(写入)和 injectIsRestoring(读取)访问,从而保证状态读写两侧的类型与语义受控。

配套读取 API:injectIsRestoring

provideIsRestoringinjectIsRestoring 是一对读写函数。读取函数签名如下:

function injectIsRestoring(options?): Signal<boolean>;

其实现(inject-is-restoring.ts)展示了 Angular 注入上下文的两条路径:

export function injectIsRestoring(options?: InjectIsRestoringOptions) {
  !options?.injector && assertInInjectionContext(injectIsRestoring)
  const injector = options?.injector ?? inject(Injector)
  return injector.get(IS_RESTORING)
}
  • 未传 injector 时:先通过 assertInInjectionContext 校验当前处于注入上下文中,否则抛出 NG0203 错误(错误信息包含 injectIsRestoring,便于定位)。
  • 传入 injector 时:可以不依赖注入上下文,通过 Injector.get 显式获取,方便在非组件代码(如手动创建的 Injector 环境)中读取。

测试 inject-is-restoring.test.ts 对上述行为做了完整覆盖:默认返回 false、能取到 provideIsRestoring 提供的外部信号值、信号更新能响应式传导、支持 injector 选项、脱离注入上下文时抛出 NG0203

谁在消费 isRestoring:查询初始化链路上的竞态防护

isRestoring 并非摆设,它被查询的底层创建逻辑内部读取。以 create-base-query.tsinjectQueryinjectInfiniteQuery 的公共基座)为例:

const isRestoring = injectIsRestoring()

随后它被用于两处关键控制:

第一处:控制乐观结果模式(第 60-63 行)

defaultedOptions._optimisticResults = isRestoring()
  ? 'isRestoring'
  : 'optimistic'

_optimisticResults 决定查询在没有任何缓存数据时如何渲染结果。普通模式下取 'optimistic',查询结果会先以乐观(loading)状态出现在信号中,UI 可立即渲染出"加载中"结构;而在恢复期间取 'isRestoring' 模式,表示当前正处于从持久化存储恢复数据的窗口期。

第二处:恢复期间跳过订阅(第 113-114 行)

const unsubscribe = isRestoring()
  ? () => undefined
  : untracked(() => observer.subscribe(...))

isRestoring()true 时,不立刻向 QueryObserver 订阅、不触发网络请求,从而避免"恢复旧数据"与"以初始状态新建查询"之间的竞态——这正是原始文档中 "injectQuery and friends also check this internally to avoid race conditions between the restore and initializing queries" 一句的源码级印证。

同样的逻辑也存在于批量查询 inject-queries.tsinjectQueries 内部同样读取 isRestoring,并对每个 query 的 defaultedOptions._optimisticResults 设置 'isRestoring''optimistic'(第 250-252 行),让并发的一批查询在恢复期间保持一致的"恢复中"行为。

生产消费方:withPersistQueryClient 如何用它驱动恢复流程

真正调用 provideIsRestoring 的是一等公民插件函数 withPersistQueryClient(来自 @tanstack/angular-query-persist-client 包),源码见 with-persist-query-client.ts。其核心流程如下:

export function withPersistQueryClient(
  persistQueryClientOptions: PersistQueryClientOptions,
): PersistQueryClientFeature {
  const isRestoring = signal(true)
  const providers = [
    provideIsRestoring(isRestoring.asReadonly()),
    {
      provide: ENVIRONMENT_INITIALIZER,
      multi: true,
      useValue: () => {
        if (!isPlatformBrowser(inject(PLATFORM_ID))) return
        const destroyRef = inject(DestroyRef)
        const queryClient = inject(QueryClient)

        const { onSuccess, onError, persistOptions } = persistQueryClientOptions
        const options = { queryClient, ...persistOptions }
        persistQueryClientRestore(options)
          .then(() => onSuccess?.())
          .catch(() => onError?.())
          .finally(() => {
            isRestoring.set(false)
            const cleanup = persistQueryClientSubscribe(options)
            destroyRef.onDestroy(cleanup)
          })
      },
    },
  ]
  return queryFeature('PersistQueryClient', providers)
}

整个生命周期由三个状态阶段构成:

  1. 初始置位:插件创建时,const isRestoring = signal(true) 立即让恢复状态为 true。由于插件在 provideTanStackQuery 初始化阶段(ENVIRONMENT_INITIALIZER)就注册完毕,任何在恢复完成前创建的查询都会读到"恢复中"状态,从而暂停订阅与请求。
  2. 平台守卫:仅在浏览器端(isPlatformBrowser)才执行恢复逻辑,SSR 环境下直接跳过,避免服务端无意义的存储读写。
  3. 恢复完成翻转persistQueryClientRestore(options)(由 @tanstack/query-persist-client-core 提供)从存储中取出序列化缓存并回填 queryClient 后,进入 finally 块把 isRestoring 翻转为 false——所有此前暂停订阅的查询随即开始正常订阅与请求,期间通过 persistQueryClientSubscribe 建立持续的持久化订阅,并在组件销毁(DestroyRef.onDestroy)时清理。

signal(true) → 恢复完成 → set(false) 的翻转,加上 provideIsRestoring(isRestoring.asReadonly()) 的只读视角暴露,形成了一条完整的"状态拥有者私有写、查询框架公共读"的信号闭环。

实战集成示例

仓库中的示例 app.config.ts 展示了完整接入方式。应用只需把 withPersistQueryClient 作为 feature 传入 provideTanStackQuery,即可让上述整套机制自动生效,无需手动调用 provideIsRestoring

import { provideHttpClient, withFetch } from '@angular/common/http'
import {
  QueryClient,
  provideTanStackQuery,
} from '@tanstack/angular-query-experimental'
import { withPersistQueryClient } from '@tanstack/angular-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import type { ApplicationConfig } from '@angular/core'

const localStoragePersister = createAsyncStoragePersister({
  storage: window.localStorage,
})

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withFetch()),
    provideTanStackQuery(
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 1000 * 60, // 1 minute
            gcTime: 1000 * 60 * 60 * 24, // 24 hours
          },
        },
      }),
      withPersistQueryClient({
        persistOptions: {
          persister: localStoragePersister,
        },
      }),
    ),
  ],
}

注意示例中把 staleTime 设为 1 分钟、gcTime 设为 24 小时,保证被持久化的缓存数据在应用冷启动后仍处于"新鲜/未被 GC"区间内,才能被 persistQueryClientRestore 有效回填。如果你的业务需要在恢复期间于 UI 上显示"正在恢复数据"的提示,可以自定义 isRestoring 信号并配合 provideIsRestoring 覆盖默认的 false 状态,再通过 injectIsRestoring 读取以驱动视图。

测试验证与行为契约

单元测试 inject-is-restoring.test.ts 从五个维度固化了 provideIsRestoring 的行为契约,可视为该 API 的权威使用规范:

测试用例 验证点
should return false by default when provideIsRestoring is not used 未注册 Provider 时,injectIsRestoring() 返回 signal(false) 的默认值
should return the provided signal value when provideIsRestoring is used 注册后,注入令牌能解析出外部传入的信号值
should reactively reflect changes to the provided signal 外部信号 set 后,读取方立即可见新值(响应式透传)
should be usable outside injection context when passing an injector 传入 injector 选项时无需注入上下文即可读取
should throw NG0203 with descriptive error outside injection context 无注入上下文且未传 injector 时抛出 NG0203(错误信息含函数名)

injectQuery / injectQueries 的测试中也可见二者协同的痕迹:例如 inject-query.test.tsinject-queries.test.ts 都通过 provideIsRestoring(signal(true).asReadonly()) 模拟"恢复进行中"来验证查询在恢复期间的挂起行为。这说明该 API 既服务于运行时,也是测试中模拟持久化场景的标准手段。

小结

  • provideIsRestoring 负责把"恢复中"布尔信号注入 Angular DI,是只写入口;injectIsRestoring 是只读入口,两者组合构成 IS_RESTORING 令牌的安全读写封装。
  • 默认未注册时为 false,对不启用持久化的应用完全无侵入。
  • injectQueryinjectInfiniteQueryinjectQueries 内部均读取该信号,通过切换 _optimisticResults 模式与跳过订阅来避免"恢复数据"与"新建查询"之间的竞态。
  • 实际业务中,@tanstack/angular-query-persist-clientwithPersistQueryClient 是它的主要调用者;普通应用开发者需要自定义恢复状态展示时,才需要直接使用 provideIsRestoring
登录后查看全文
热门项目推荐
相关项目推荐