TanStack Query FocusManager 完全指南:掌控焦点状态与窗口重聚焦时的数据刷新
【免费下载链接】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
导读
FocusManager是 TanStack Query 核心层(query-core)中负责统一管理"窗口/页面焦点状态"的单例管理器,它决定了查询在用户切换到其他标签页再返回时是否自动重新获取数据。本文将以官方参考文档为主体,结合仓库源码,深入讲解FocusManager的四个核心方法(setEventListener、subscribe、setFocused、isFocused),剖析它如何接入浏览器的visibilitychange事件、如何与重试机制(retryer)、QueryClient挂载生命周期及refetchInterval定时刷新协同工作,并给出覆盖 React、Vue、Svelte 等框架的实战用法与测试验证。读完本文,你将能完全掌控 TanStack Query 的窗口焦点感知行为,并能针对特定框架或业务场景自定义焦点事件源。
一、FocusManager 是什么
FocusManager在 TanStack Query 中负责管理"焦点状态"(focus state)。默认情况下,TanStack Query 会在浏览器窗口重新获得焦点(例如用户切走标签页后再切回来)时,自动重新获取(refetch)那些处于 stale 状态的查询——这一"魔法"行为的底层就是FocusManager。
它的两大职责(来自官方文档):
- 改变默认的事件监听器(用于适配 React Native、Web 之外的运行环境或自定义事件源);
- 手动改变焦点状态(用于在自动化测试、或需要人为控制刷新时机时直接注入状态)。
官方文档给出的可用方法共四个,全部可直接通过框架包导出的单例调用:
setEventListenersubscribesetFocusedisFocused
在仓库中,FocusManager类位于 packages/query-core/src/focusManager.ts,并通过export const focusManager = new FocusManager()导出一个全局单例,同时从 packages/query-core/src/index.ts 对外导出。各框架包(如react-query、vue-query、svelte-query)均从@tanstack/query-core再导出该单例,因此无论你使用哪个框架,都可以这样导入:
import { focusManager } from '@tanstack/react-query' // Vue / Svelte / Solid 等其他框架同理: // import { focusManager } from '@tanstack/vue-query' // import { focusManager } from '@tanstack/svelte-query' // import { focusManager } from '@tanstack/solid-query'二、focusManager.setEventListener:自定义焦点事件源
2.1 官方用法
setEventListener用于设置自定义的事件监听器。当默认的visibilitychange监听不适用于你的运行环境时(例如 React Native、Electron、小程序容器),可以用它替换掉默认实现:
import { focusManager } from '@tanstack/react-query' focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('visibilitychange', handleFocus, false) } return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', handleFocus) } })setEventListener接收一个setup函数:TanStack Query 会把一个handleFocus回调传给它,setup 函数负责把该回调挂到任意事件源上,并返回一个"清理函数",用于在新监听器替换或订阅全部取消时解除绑定。
2.2 源码级解析:替换与清理机制
从源码 packages/query-core/src/focusManager.ts 可以看到setEventListener的实现:
setEventListener(setup: SetupFn): void { this.#setup = setup this.#cleanup?.() this.#cleanup = setup((focused) => { if (typeof focused === 'boolean') { this.setFocused(focused) } else { this.onFocus() } }) }三个关键点:
- 先清理再替换:每次调用
setEventListener都会先执行上一次 setup 返回的#cleanup,确保旧的visibilitychange监听被移除,不会发生事件泄漏或重复触发; - 回调参数可带值可空:传入 setup 的
handleFocus既可以接收boolean(直接设置焦点状态),也可以无参调用(走默认的焦点判定逻辑onFocus()); - 默认 setup:在
FocusManager构造函数中(focusManager.ts)内置了默认 setup——当typeof window !== 'undefined' && window.addEventListener时,监听visibilitychange事件并返回对应的removeEventListener清理函数。值得注意的是源码注释明确说明:addEventListener在 React Native 中不存在,但window存在,因此该守卫是必须的。
测试 packages/query-core/src/tests/focusManager.test.tsx 中验证了这些行为:
- "should call previous remove handler when replacing an event listener":连续两次
setEventListener,第一次的 remove 函数会被调用一次,第二次的不会被调用——印证了"替换即清理"; - "cleanup (removeEventListener) should not be called if window is not defined" 与 "…if window.addEventListener is not defined":在
window不存在或window.addEventListener不存在时,取消订阅不会调用removeEventListener,印证了默认 setup 的环境守卫逻辑。
2.3 实战:React Native 自定义事件源
在 React Native 中,标准的visibilitychange并不存在,社区常用AppStateAPI 来感知前后台切换。你可以这样接入:
import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' focusManager.setEventListener((handleFocus) => { const subscription = AppState.addEventListener('change', (status) => { handleFocus(status === 'active') }) return () => subscription.remove() })这样当 App 从后台回到前台(active)时,TanStack Query 就会像 Web 端切回标签页一样触发焦点恢复逻辑。
三、focusManager.subscribe:订阅焦点状态变化
subscribe用于订阅可见性/焦点状态的变化,并返回一个取消订阅的函数(官方文档强调:"It returns an unsubscribe function"):
import { focusManager } from '@tanstack/react-query' const unsubscribe = focusManager.subscribe((isVisible) => { console.log('isVisible', isVisible) }) // 不再需要时取消订阅 unsubscribe()3.1 底层机制:继承自 Subscribable
FocusManager继承自 packages/query-core/src/subscribable.ts 中的Subscribable基类:
subscribe(listener)将监听器加入Set,触发onSubscribe(),返回一个删除监听器并触发onUnsubscribe()的取消函数;FocusManager覆写了onSubscribe/onUnsubscribe(focusManager.ts):当第一个订阅者出现时,才通过setEventListener(this.#setup)挂载事件监听;当最后一个订阅者取消后,执行#cleanup并置空,实现"按需挂载、零订阅零开销"。
测试 "should call removeEventListener when last listener unsubscribes"(focusManager.test.tsx)验证了:两个订阅者只注册一次visibilitychange,第二个取消订阅时才真正移除监听。
3.2 谁在订阅:QueryClient 挂载时接入
QueryClient.mount()(packages/query-core/src/queryClient.ts)正是通过subscribe与焦点状态联动的:
mount(): void { this.#mountCount++ if (this.#mountCount !== 1) return this.#unsubscribeFocus = focusManager.subscribe(async (focused) => { if (focused) { await this.resumePausedMutations() this.#queryCache.onFocus() } }) // ... }即:窗口重新获得焦点时,QueryClient会先恢复被暂停的 mutation,再通知QueryCache对所有受影响的查询执行onFocus刷新(queryCache.onFocus()会调用查询的onFocus()以重新获取数据)。这也解释了为何默认行为是"切回标签页自动刷新"。当unmount()且挂载计数归零时,对应订阅会被取消,避免内存泄漏。
四、focusManager.setFocused:手动控制焦点状态
setFocused用于手动设置焦点状态。传undefined时,则回退到默认的焦点检查逻辑(即基于document.visibilityState的判定):
import { focusManager } from '@tanstack/react-query' // Set focused focusManager.setFocused(true) // Set unfocused focusManager.setFocused(false) // Fallback to the default focus check focusManager.setFocused(undefined)Options
focused: boolean | undefined
4.1 源码行为:变更才通知
从源码 focusManager.ts 可以看到:
setFocused(focused?: boolean): void { const changed = this.#focused !== focused if (changed) { this.#focused = focused this.onFocus() } }只有状态实际发生变化时才会广播给所有监听者;连续设置相同的值不会触发通知。测试 "should call listeners when setFocused is called"(focusManager.test.tsx)精确验证了这一点:连续两次setFocused(true)只通知一次;随后setFocused(undefined)会回退到默认判定(测试环境中document被模拟为可见,因此回调收到true)。
4.2 实战场景
- 测试场景:在 Vitest/Jest 中模拟窗口失焦/聚焦,无需真实触发浏览器事件:
// 模拟失焦,让重试与定时刷新暂停 focusManager.setFocused(false) // 恢复聚焦 focusManager.setFocused(true)- 后台预取场景:某些场景下你希望在页面"名义上失焦"时仍不中断查询,或反过来强制让查询感知到焦点,均可通过此 API 精确控制。
五、focusManager.isFocused:读取当前焦点状态
isFocused返回当前焦点状态(boolean):
const isFocused = focusManager.isFocused()5.1 默认判定逻辑
源码 focusManager.ts 的默认实现:
isFocused(): boolean { if (typeof this.#focused === 'boolean') { return this.#focused } // document global can be unavailable in react native return globalThis.document?.visibilityState !== 'hidden' }- 若此前通过
setFocused(boolean)手动设置过,则直接返回该值; - 否则基于
globalThis.document?.visibilityState !== 'hidden'判定:即只要页面不是hidden状态就算"聚焦"。源码注释特别说明:document全局对象在 React Native 中可能不存在,因此使用了可选链,此时判定为true(不会误判为失焦)。测试 "should return true for isFocused if document is undefined"(focusManager.test.tsx)专门验证了删除globalThis.document后isFocused()返回true的边界行为。
六、FocusManager 在数据获取链路中的关键作用
FocusManager 并非孤立存在,它在查询生命周期中承担着"节流阀"的角色,直接影响数据获取的启停。
6.1 重试暂停:retryer.ts
查询的获取与重试由 packages/query-core/src/retryer.ts 中的createRetryer驱动。其canContinue判定(retryer.ts)为:
const canContinue = () => focusManager.isFocused() && (config.networkMode === 'always' || onlineManager.isOnline()) && config.canRun()当查询失败进入重试等待时,如果窗口失焦或设备离线,run循环会调用pause()挂起重试(源码注释明确写着 "Pause if the document is not visible or when the device is offline");一旦重新聚焦(focusManager状态变化),等待中的重试继续执行。这正是"失焦时停止无谓请求、聚焦时无缝续传"的核心机制。
6.2 定时刷新守卫:queryObserver.ts
在QueryObserver的定时重取逻辑中(packages/query-core/src/queryObserver.ts),refetchInterval的每次触发都会检查焦点:
this.#refetchIntervalId = timeoutManager.setInterval(() => { if ( this.options.refetchIntervalInBackground || focusManager.isFocused() ) { this.#executeFetch() } }, this.#currentRefetchInterval)即:默认情况下,窗口失焦时定时刷新被跳过;只有显式设置refetchIntervalInBackground: true才在后台继续刷新。
6.3 焦点恢复联动:queryClient.ts+queryCache.ts
如 3.2 节所述,QueryClient.mount()订阅焦点变化后调用queryCache.onFocus()(packages/query-core/src/queryCache.ts),后者遍历所有查询触发各自的query.onFocus(),完成"回到页面即刷新 stale 数据"的默认行为。
七、与在线状态管理器的对比
FocusManager与OnlineManager结构对称、职责互补:FocusManager感知"焦点",OnlineManager感知"网络"。二者都以单例形式存在于query-core,都提供setEventListener/subscribe/setFocused(isOnline)/isFocused(isOnline)这类同构 API,并且在retryer.ts的canContinue中被并列判断(见 6.1 节)。如果你需要为离线优先的应用自定义网络状态判定,可参考对应的 onlineManager.md 文档,其使用模式与本文完全一致。
八、要点总结
| 方法 | 作用 | 典型场景 |
|---|---|---|
setEventListener(setup) | 替换默认事件监听器(默认监听visibilitychange) | React Native / Electron / 自定义事件源 |
subscribe(listener) | 订阅焦点状态变化,返回取消函数 | 与QueryClient.mount()联动、自定义副作用 |
setFocused(boolean \| undefined) | 手动设置焦点状态,undefined回退默认判定 | 测试模拟、强制控制刷新 |
isFocused() | 读取当前焦点状态 | 重试暂停/恢复、定时刷新守卫 |
核心要点回顾:
- 默认事件源是浏览器的
visibilitychange(focusManager.ts),且带window.addEventListener存在性守卫,兼容 React Native; - 替换即清理:
setEventListener会先执行旧监听器的清理函数,杜绝事件泄漏; - 懒挂载:只有出现第一个订阅者才挂载事件监听,最后一个取消订阅后立即清理(subscribable.ts);
- 变更才通知:
setFocused仅在状态实际变化时广播(focusManager.test.tsx); - 三处核心消费方:
retryer的重试暂停(retryer.ts)、QueryClient.mount()的焦点恢复刷新(queryClient.ts)、QueryObserver的定时刷新守卫(queryObserver.ts)。
无论你是要适配非浏览器环境、优化移动端体验,还是要编写稳定可靠的测试,FocusManager都是你必须掌握的 TanStack Query 核心基础设施。
【免费下载链接】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),仅供参考