Angular Query 中 provideIsRestoring 解析:信号化持久化恢复状态,避免查询与数据恢复之间的竞态
Angular Query(TanStack Query Angular 适配,仓库中位于 packages/angular-query-experimental)通过 provideIsRestoring 将"是否正在恢复持久化查询数据"的状态以 Angular Signal 的形式注入依赖注入容器,供 injectQuery、injectQueries 等 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-client 的 withPersistQueryClient)在内部调用,普通应用开发者通常不需要直接编写业务代码去使用它;当你想自定义一套"恢复期间的状态展示"逻辑时,它才成为开放的扩展点。
该函数与配套的读取函数 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,
}
}
两点关键设计值得注意:
- 默认值为
false:注入令牌在未调用provideIsRestoring时,通过factory提供一个恒为false的只读信号。这意味着不启用持久化功能的普通应用中,isRestoring永远是false,查询走常规的乐观更新路径,行为与旧版无差异。 useValue直接引用传入信号:Provider 不复制信号、也不包装一层新信号,而是把外部传入的信号对象本身作为令牌的值。因此外部信号后续set/update引起的变化会响应式地传导到所有injectIsRestoring()的读取方——这正是测试should reactively reflect changes to the provided signal所验证的行为。
需要说明的是,IS_RESTORING 令牌本身是模块私有常量(源码注释标注为 "Internal token"),外部无法直接注入该令牌,只能通过公共 API provideIsRestoring(写入)和 injectIsRestoring(读取)访问,从而保证状态读写两侧的类型与语义受控。
配套读取 API:injectIsRestoring
provideIsRestoring 与 injectIsRestoring 是一对读写函数。读取函数签名如下:
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.ts(injectQuery 与 injectInfiniteQuery 的公共基座)为例:
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.ts:injectQueries 内部同样读取 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)
}
整个生命周期由三个状态阶段构成:
- 初始置位:插件创建时,
const isRestoring = signal(true)立即让恢复状态为true。由于插件在provideTanStackQuery初始化阶段(ENVIRONMENT_INITIALIZER)就注册完毕,任何在恢复完成前创建的查询都会读到"恢复中"状态,从而暂停订阅与请求。 - 平台守卫:仅在浏览器端(
isPlatformBrowser)才执行恢复逻辑,SSR 环境下直接跳过,避免服务端无意义的存储读写。 - 恢复完成翻转:
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.ts 与 inject-queries.test.ts 都通过 provideIsRestoring(signal(true).asReadonly()) 模拟"恢复进行中"来验证查询在恢复期间的挂起行为。这说明该 API 既服务于运行时,也是测试中模拟持久化场景的标准手段。
小结
provideIsRestoring负责把"恢复中"布尔信号注入 Angular DI,是只写入口;injectIsRestoring是只读入口,两者组合构成IS_RESTORING令牌的安全读写封装。- 默认未注册时为
false,对不启用持久化的应用完全无侵入。 injectQuery、injectInfiniteQuery、injectQueries内部均读取该信号,通过切换_optimisticResults模式与跳过订阅来避免"恢复数据"与"新建查询"之间的竞态。- 实际业务中,
@tanstack/angular-query-persist-client的withPersistQueryClient是它的主要调用者;普通应用开发者需要自定义恢复状态展示时,才需要直接使用provideIsRestoring。
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 StartedRust0626
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