TanStack Query notifyManager 源码级解析:回调调度、批量更新与自定义通知机制
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
notifyManager是 TanStack Query 框架无关核心(query-core)中负责调度与批量执行回调的轻量级管理器,它决定了状态更新何时、以何种方式通知到各个 Observer 与订阅者。本文基于 docs/reference/notifyManager.md 展开,并结合 notifyManager.ts 源码、测试用例及各框架适配层,完整讲解batch、batchCalls、schedule、setNotifyFunction、setBatchNotifyFunction、setScheduler六个核心 API 的原理与实战用法。读完你将掌握:为什么 QueryClient 的批量操作能合并渲染、React 测试中如何用act包裹通知、Solid Query 如何复用 Solid 的batch,以及如何自定义调度时机(微任务 / 动画帧 / 延时)。
一、notifyManager 是什么:调度与批量的"总开关"
TanStack Query 的核心是一个发布-订阅模型:Query/Mutation状态变化后需要通知QueryObserver等订阅者,进而触发框架层的重新渲染。如果每次状态微调都同步触发一次通知,很容易造成重复渲染和性能浪费。
notifyManager就是为了解决这个问题而存在的单例工具,它统一负责三件事:
- 调度(schedule):决定回调在哪个时机执行(默认通过
setTimeout(callback, 0)延迟到下一个事件循环); - 批量(batch):把同一事务内产生的多次更新合并成一次通知;
- 可定制(setter):把"通知函数""批量函数""调度器"全部抽象成可替换的接口,让 React、Solid、Preact、Angular、Vue 等不同框架都能接入自己平台的批处理机制。
在仓库中,notifyManager由 packages/query-core/src/notifyManager.ts 定义,并通过createNotifyManager()创建后以单例形式导出,整个 query-core 及其上层框架包共享同一个实例:
// SINGLETON export const notifyManager = createNotifyManager()文档中说明它暴露以下方法,本文逐一展开:
| 方法 | 作用 |
|---|---|
batch(callback) | 批量执行:callback 内所有 schedule 的更新在事务结束后统一 flush |
batchCalls(callback) | 高阶函数:包装后的函数所有调用都会被调度到下一个 batch |
schedule(callback) | 调度一个函数在下一个 batch 中运行 |
setNotifyFunction(fn) | 覆盖"执行回调"的函数(默认直接调用) |
setBatchNotifyFunction(fn) | 设置批量更新的执行函数(如 Solid 的batch) |
setScheduler(fn) | 配置"何时运行下一个 batch"的调度器(默认setTimeout(cb, 0)) |
二、batch:把多次更新合并为一次通知
function batch<T>(callback: () => T): Tbatch用于把传入 callback 内部触发的所有更新调度合并起来,在 callback 执行完毕后再统一通知。文档明确指出:它主要被内部用来优化queryClient的更新。
真实调用证据
在 queryClient.ts 中,多个批量 API 都用notifyManager.batch包裹。例如setQueriesData会同时更新多个 query 的数据,整个遍历过程被包在一个batch内,避免每个 query 各自触发一次订阅者通知:
// packages/query-core/src/queryClient.ts#L225-L232 return notifyManager.batch(() => this.#queryCache .findAll(filters) .map(({ queryKey }) => [ queryKey, this.setQueryData<TQueryFnData>(queryKey, updater, options), ]), )类似的用法还遍布于:
- queryClient.ts 的
removeQueries(第 254 行)、resetQueries(第 267 行)、cancelQueries(第 289 行); - queryCache.ts 的
find/remove/clear等内部通知; - mutation.ts 的
dispatch,在一次 mutation 状态变更时同时通知所有 observers 和 mutationCache(第 397–406 行):
// packages/query-core/src/mutation.ts#L397-L406 notifyManager.batch(() => { this.#observers.forEach((observer) => { observer.onMutationUpdate(action) }) this.#mutationCache.notify({ mutation: this, type: 'updated', action, }) })batch支持嵌套(内部再调用batch),并且即使 callback 抛出异常也会保证 flush 发生——这一点有测试用例专门覆盖(见下文"测试验证"一节),确保事务不会因异常而泄漏未完成的通知。
三、batchCalls:把任意回调变成"批量调度版"
type BatchCallsCallback<T extends Array<unknown>> = (...args: T) => void function batchCalls<T extends Array<unknown>>( callback: BatchCallsCallback<T>, ): BatchCallsCallback<T>batchCalls是一个高阶函数:传入一个回调,返回一个包装后的新函数。对包装函数的所有调用都不会立即执行原回调,而是通过schedule把它排队到下一个 batch 统一执行——效果等同于"把多次事件触发合并成一次通知"。
这是框架适配层最常用的 API 之一。例如在 Preact Query 中,Observer 的订阅回调被包装后交给 store:
// packages/preact-query/src/useBaseQuery.ts#L99 ? observer.subscribe(notifyManager.batchCalls(onStoreChange))在 Angular Query(experimental)中,injectQueries等多个组合式 API 也用它包裹状态更新回调,把来自 query-core 的高频通知合批后再驱动 Angular 变更检测:
// packages/angular-query-experimental/src/inject-queries.ts#L303 notifyManager.batchCalls((state) => { ... })类型层面,batchCalls保持参数签名不变,测试文件 notifyManager.test.tsx 第 82–98 行专门用expectTypeOf校验包装前后参数类型完全一致(传入(a: string, b: number) => string,包装函数仍接受相同签名,错误参数会被 TypeScript 报错)。
四、schedule:调度回调到下一个 batch
function schedule(callback: () => void): voidschedule把回调安排到"下一个 batch"运行。默认情况下,这个 batch 通过setTimeout执行,但可以通过setScheduler完全替换(详见第六节)。
从源码看,schedule的逻辑是:
// packages/query-core/src/notifyManager.ts const schedule = (callback: NotifyCallback): void => { if (transactions) { // 处于 batch 事务中:入队,等待 flush queue.push(callback) } else { // 不在事务中:直接交给调度器 scheduleFn(() => { notifyFn(callback) }) } }关键在于transactions计数器:当处于batch事务内时,回调只进队列不执行;事务结束后由flush统一派发。因此在batch之外调用schedule就等于"尽快异步执行"。
Preact Query 的useMutationState就单独用它来安排 store 更新:
// packages/preact-query/src/useMutationState.ts#L186 notifyManager.schedule(onStoreChange)五、setNotifyFunction:接管"通知执行",典型场景是 React.act
notifyManager.setNotifyFunction(fn)setNotifyFunction用来覆盖真正执行回调的那个函数。默认的notifyFunction只是直接调用回调(源码第 20–22 行):
let notifyFn: NotifyFunction = (callback) => { callback() }文档给出了最经典的实战场景:在 React 测试中把通知用act包裹,让 React 在受控环境下处理状态更新,避免测试中的 act 警告:
import { notifyManager } from '@tanstack/react-query' import { act } from 'react-dom/test-utils' notifyManager.setNotifyFunction(act)这一模式在仓库的测试基建中被大量复用。例如 packages/preact-query/test-setup.ts 第 12 行、packages/react-query/test-setup.ts、packages/query-broadcast-client-experimental/test-setup.ts 等均通过setNotifyFunction把测试环境中的通知收敛进框架的批处理上下文。源码注释也明确写着这一用途(notifyManager.ts 第 79–81 行):
/** * Use this method to set a custom notify function. * This can be used to for example wrap notifications with `React.act` while running tests. */ setNotifyFunction: (fn: NotifyFunction) => { notifyFn = fn },六、setBatchNotifyFunction:接入框架自有批处理能力
notifyManager.setBatchNotifyFunction(fn)setBatchNotifyFunction设置用于批量更新的函数。默认的batchNotifyFn同样只是直接调用(源码第 23–25 行):
let batchNotifyFn: BatchNotifyFunction = (callback: () => void) => { callback() }文档强调:如果你的框架支持自定义批处理函数,可以把它告诉 TanStack Query。最典型的例子是 Solid Query——Solid 自带batch,于是把notifyManager的批量执行直接替换为 Solid 的batch,从而让通知进入 Solid 的响应式批处理周期:
import { notifyManager } from '@tanstack/query-core' import { batch } from 'solid-js' notifyManager.setBatchNotifyFunction(batch)对应源码注释(notifyManager.ts 第 86–88 行)也说明:默认情况下 React Query 会使用 ReactDOM / React Native 提供的 batch 函数。也就是说,setBatchNotifyFunction是 TanStack Query 实现框架无关的关键机制之一——核心层只定义"批量"这一抽象,具体语义由各框架注入。
在 Solid Query 的源码中也能看到同样的思路:useQueries内部用 Solid 的batch包裹状态写入,例如 packages/solid-query/src/useQueries.ts 第 262 行与第 275 行,以及useQuery.test.tsx中 "should batch re-renders" 的测试用例,验证了批量渲染行为。
七、setScheduler:自定义"下一个 batch"何时运行
notifyManager.setScheduler(fn)setScheduler配置的是一个调度器:它接收一个回调,负责决定"下一个 batch 什么时候运行"。默认行为等价于setTimeout(callback, 0)。文档给出了三种典型的自定义方案:
import { notifyManager } from '@tanstack/react-query' // 在下一个微任务中调度 batch(比 setTimeout 更早执行) notifyManager.setScheduler(queueMicrotask) // 在下一帧渲染之前调度 batch(适合与绘制节奏对齐) notifyManager.setScheduler(requestAnimationFrame) // 延迟一段时间后再执行 batch notifyManager.setScheduler((cb) => setTimeout(cb, 10))从源码看,默认调度器并不直接写setTimeout,而是引用了 timeoutManager.ts 导出的systemSetTimeoutZero(第 136–138 行),其实现就是:
// packages/query-core/src/timeoutManager.ts export function systemSetTimeoutZero(callback: TimeoutCallback): void { setTimeout(callback, 0) }这种封装是为了让query-core统一审计系统setTimeout的使用,避免在不可信环境下直接依赖全局定时器。
八、源码内部机制:队列、事务与 flush
理解notifyManager只需抓住三个内部状态(notifyManager.ts 第 18–26 行):
let queue: Array<NotifyCallback> = [] // 待通知回调队列 let transactions = 0 // 事务计数(batch 嵌套深度) let notifyFn: NotifyFunction = (callback) => { callback() } let batchNotifyFn: BatchNotifyFunction = (callback) => { callback() } let scheduleFn = defaultScheduler // 默认 setTimeout(cb, 0)完整执行流程:
- 调用
batch(callback)→transactions++,执行 callback; - callback 内部的
schedule由于transactions > 0,只把回调推入 queue; - callback 执行结束(无论成功或抛错,
finally保证)→transactions--;当transactions归零时调用flush(); flush取出整个队列并重置,再通过调度器安排一次批量派发:scheduleFn(决定何时执行);- 执行时先调
batchNotifyFn(框架可自定义,如 Solid 的batch); - 批量内部对每个回调调用
notifyFn(框架可自定义,如 React 测试的act)。
// packages/query-core/src/notifyManager.ts const flush = (): void => { const originalQueue = queue queue = [] if (originalQueue.length) { scheduleFn(() => { batchNotifyFn(() => { originalQueue.forEach((callback) => { notifyFn(callback) }) }) }) } }batchCalls的底层实现就是"包装后的每次调用都走schedule",与batch配合即实现"多次调用、一次通知":
// packages/query-core/src/notifyManager.ts batchCalls: <T extends Array<unknown>>(callback) => { return (...args) => { schedule(() => { callback(...args) }) } },另外,createNotifyManager每次调用都会创建独立的闭包状态,这为测试提供了便利——测试中可以createNotifyManager()得到互不干扰的实例(见下文)。
九、测试验证:行为有据可查
packages/query-core/src/tests/notifyManager.test.tsx 使用 Vitest 对上述行为做了完整覆盖,可以作为理解每个 API 语义的"活的文档":
- 默认 notifyFn(第 23–29 行):
schedule(callback)后推进假定时器,回调被调用; - 默认 batchNotifyFn 与嵌套 batch(第 31–46 行):
batch(batch(batch(schedule)))三层嵌套只各执行一次,证明事务计数器正确归零后才 flush; - 自定义 scheduler(第 48–62 行):
setScheduler(queueMicrotask)后,调度器被调用且 notify 最终执行; - 异常安全(第 64–80 行):
batch内schedule后抛出错误,捕获后推进定时器,通知仍然执行——验证finally中的 flush 逻辑; - batchCalls 类型保持(第 82–98 行):包装函数参数签名与原始回调完全一致;
- 自定义 batchNotifyFunction(第 100–118 行):同一 batch 内的多个
schedule回调经批量函数统一执行; - batchCalls 行为(第 120–132 行):包装函数以原始参数调用后,回调在下一批被执行。
这些测试同时证明了notifyManager的可测试性设计:createNotifyManager()可创建隔离实例,配合vi.useFakeTimers()即可精准控制调度时机。
十、总结与使用建议
notifyManager是 TanStack Query 全部状态通知的"调度中枢",其设计可以概括为三层抽象:
| 层级 | 默认实现 | 可替换 API | 典型替换 |
|---|---|---|---|
| 何时执行(调度) | setTimeout(cb, 0) | setScheduler | queueMicrotask、requestAnimationFrame |
| 如何批量 | 直接调用 | setBatchNotifyFunction | Solid 的batch、ReactDOM 的批处理 |
| 如何通知 | 直接调用 | setNotifyFunction | React 测试中的act |
对普通使用者而言,日常开发通常不需要直接操作notifyManager——batch的优化在queryClient.setQueriesData、removeQueries、mutation 派发等内部路径已经自动生效。但当你遇到以下场景时,它就变得不可或缺:
- 写框架适配层:用
batchCalls包装订阅回调,把高频通知合批后驱动自己的渲染管线; - 写测试:用
setNotifyFunction(act)消除 React 的 act 警告; - 对接自有框架 / 调度策略:用
setBatchNotifyFunction接入平台的批处理能力,用setScheduler控制通知时机(如与动画帧对齐)。
深入阅读建议:notifyManager 源码、notifyManager 测试、queryClient 中的批量应用、solid-query 的 batch 集成。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考