React Query 底层原理与组件更新机制
React Query 把数据层和视图层彻底解耦:数据状态存在 React 之外的缓存里,组件通过 Observer 订阅,再用 useSyncExternalStore 接入渲染。理解这条「缓存 → Observer → Hook」的链路,就能解释它如何做到多组件共享数据、按字段精确更新和请求去重。
一、四层架构
QueryClient (全局单例,持有 cache)
└── QueryCache (Map<queryHash, Query>,数据真正的容器)
└── Query (单条数据的状态机:data / status / fetch)
└── QueryObserver (订阅者,桥接组件)
└── useQuery (React Hook)| 层 | 职责 |
|---|---|
QueryClient | 全局唯一,持有 cache,是所有操作的入口 |
QueryCache | Map<queryHash, Query>,缓存数据的真正容器 |
Query | 单个查询的状态机,管理 data / status / fetch,与组件无关 |
QueryObserver | 关键中间层,一个组件对应一个 observer,订阅 Query 并通知组件 |
二、为什么需要 Observer 这一层
数据(Query)是全局共享的,组件却是局部的,同一个 queryKey 可能被多个组件使用。Observer 在「共享数据」和「局部组件」之间做桥接:
- 多组件共享同一份数据:N 个 observer 订阅同一个 Query,数据只存一份。
- 按需通知:每个 observer 持有自己的
select、enabled配置,只在它关心的字段变化时才触发对应组件更新。
三、通知组件更新:核心是 useSyncExternalStore
React Query 没有用 Context 透传数据,而是 发布订阅 + useSyncExternalStore。简化后的实现:
function useBaseQuery(options: QueryOptions) {
const client = useQueryClient()
// 复用同一个 observer,避免每次渲染重建订阅
const [observer] = React.useState(() => new QueryObserver(client, options))
return React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => observer.subscribe(onStoreChange), // 订阅
[observer],
),
() => observer.getOptimisticResult(options), // getSnapshot
() => observer.getOptimisticResult(options), // getServerSnapshot
)
}完整通知链路:
四、关键优化:不是所有变化都通知
Observer 内部用三层手段过滤无效渲染:
select选择器:组件只订阅select返回值的变化,原始 data 其它部分变了也不触发。notifyOnChangeProps: 'tracked'(默认):通过 getter 代理追踪组件实际读取了哪些字段,只有用到的字段变化才 re-render。
function TodoCount() {
// 只读了 data,没读 isFetching
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// 因此仅当 data 变化才重渲染,isFetching 翻转不会触发
return <span>{data?.length ?? 0}</span>
}- 结构共享(structural sharing):新旧数据深度对比,引用未变的部分复用旧引用,让
===比较生效,配合useMemo/select能进一步减少下游渲染。
五、补充
为什么用 useSyncExternalStore 而不是 useState?
QueryCache 的数据变更发生在 React 之外。useSyncExternalStore 是 React 18 为外部数据源提供的官方订阅 API,能正确处理并发模式下的 tearing(撕裂),保证一次渲染内读到的快照一致。v4 之前用的是手写 forceUpdate,并发模式下不安全。
缓存数据为什么不放 Context? Context 中任意值变化都会让所有消费者 re-render,无法做细粒度控制,也不适合频繁变化的数据。外部 store + observer 才能做到精确的、按字段的更新。
多个组件用同一个 key,fetch 发几次?
一次。Query 是共享的,多个 observer 订阅同一个 Query,queryFn 在去重窗口内只执行一次(请求去重 / dedupe)。
一句话记忆:React Query 把数据放在 React 之外的 QueryCache,用 Observer 做发布订阅、用
useSyncExternalStore接入渲染,再靠tracked字段追踪和结构共享实现按需、精确的组件更新——既共享数据又避免无效渲染。