news 2026/9/24 16:50:18

Relay 18 缓存复用指南:深入解析 Fetch Policies 四种取值与 store 数据可用性判定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 18 缓存复用指南:深入解析 Fetch Policies 四种取值与 store 数据可用性判定

Relay 在应用运行过程中会把多次查询获取到的数据缓存到本地 store 中。为了让页面在切换 Tab、返回已访问过的帖子详情页时能立即渲染、跳过网络等待,我们需要借助fetchPolicy明确告诉 Relay 何时该命中本地缓存、何时该发起网络请求。本文基于仓库内 v18.0.0 版本文档 fetch-policies.md 展开,结合react-relayrelay-runtime的源码实现,讲清四种 fetch policy 的行为差异、store数据"可用性"的判定机制,以及与之配套的数据保留、失效与部分渲染方案,读完即可在真实项目中落地配置。

复用缓存的第一步:把fetchPolicy传给loadQuery

复用本地缓存数据的第一步,是向loadQuery函数传入一个fetchPolicyloadQuery通常由useQueryLoaderHook 提供,完整的调用关系在 Fetching Queries 章节 中有详细介绍。下面是一个完整的示例:

const React = require('React'); const {graphql} = require('react-relay'); function AppTabs() { const [ queryRef, loadQuery, ] = useQueryLoader(HomeTabQuery); const onSelectHomeTab = () => { loadQuery({id: '4'}, {fetchPolicy: 'store-or-network'}); } // ... }

传入的fetchPolicy将决定两件事:

  • 是否应该从本地缓存(store)中满足查询;
  • 根据该查询的数据在 store 中的 可用性,是否需要发起网络请求从服务端获取查询结果。

从源码结构看,useQueryLoader内部(packages/react-relay/relay-hooks/useQueryLoader.js)会把options中的fetchPolicynetworkCacheConfig透传给真正的 loadQuery.js 实现;loadQuery在每次调用时会生成新的fetchKey,保证同一个查询在多次调用时会被独立评估,避免 Suspense 缓存误复用旧的查询引用(见 loadQuery.js)。

四种 Fetch Policy 的完整语义

默认情况下,Relay 会先尝试从本地缓存读取查询;如果该查询的任何数据 缺失 或 过期,它会从网络获取整个查询。这个默认策略就叫"store-or-network"

具体来说,fetchPolicy可以是以下四种取值之一:

取值复用本地缓存发起网络请求适用场景
"store-or-network"(默认)仅当查询有数据缺失或过期时才发起;若查询已完全缓存,则不发起大多数场景:能复用缓存就复用,必要时才回源
"store-and-network"始终发起,无论 store 中数据是否缺失或过期需要即时刷新且不想等待(先用缓存渲染,同时后台更新)
"network-only"不会始终发起,完全忽略本地缓存及其缺失/过期状态对数据新鲜度要求极高、必须走后端最新数据
"store-only"只会永不发起网络请求纯本地数据(如 client-only 数据)读取与操作,或由调用方自行负责拉取

这四种取值的类型定义可以在 packages/relay-runtime/util/RelayRuntimeTypes.js 中找到:

export type FetchQueryFetchPolicy = 'store-or-network' | 'network-only'; export type FetchPolicy = FetchQueryFetchPolicy | 'store-and-network' | 'store-only';

注意,Refetching 章节 中讨论的refetch函数同样接受一个fetchPolicy参数,因此上述四种策略在"重新获取不同数据"的场景下同样适用。

源码中的关键实现:store 检查与网络请求的取舍

在 packages/react-relay/relay-hooks/loadQuery.js 的checkAvailabilityAndExecute中,可以清楚地看到这四种策略的分流逻辑:

const shouldFetch = fetchPolicy !== 'store-or-network' || environment.check(operation).status !== 'available';

也就是说:

  • 当策略不是store-or-network时,shouldFetch恒为true,一定会走executeDeduped发起网络执行;
  • 当策略是store-or-network时,才真正调用environment.check(operation)检查 store 中数据是否available(可用),不可用才发起请求。

这里environment.check的返回结果就是 store 对一次查询的可用性判定。该类型在 packages/relay-runtime/store/RelayStoreTypes.js 中定义:

export type OperationAvailability = | {status: 'available', fetchTime: ?number} | {status: 'stale'} | {status: 'missing'};

三种状态含义如下:

  • available:查询所需的全部本地数据都存在且未过期,可以直接从 store 渲染,不需要网络请求;
  • missing:查询有部分数据在 store 中缺失,必须发起网络请求;
  • stale:数据存在,但已被标记为过期(例如记录被显式失效,或超过查询缓存过期时间),需要重新获取。

在 RelayModernEnvironment.check 中,如果配置了missingFieldHandlers或查询涉及客户端抽象类型,则会走_checkSelectorAndHandleMissingFields,先通过 missing field handlers 尝试补齐缺失字段,再最终判定;否则直接委托给this._store.check(operation)。这就是为什么"数据缺失"的判定并不仅仅是看 store 里有没有,还受缺失字段处理器影响。

store 中数据可用性由什么决定

fetch policy 的行为取决于查询评估那一刻 store 中数据的可用性。可用性由两个因素决定:数据的存在性(presence)与数据的过期性(staleness),详见 availability-of-data.md。

数据的存在性与垃圾回收

要复用 store 中的缓存数据,首先要理解这些数据的生命周期:数据是否存在于 store 中、能存在多久。一般地,一个查询在首次被获取之后,只要它还在屏幕上被渲染,其数据就会存在于 store 中;如果某个查询从未被获取过,那它的数据自然就是缺失的。

但应用运行越久,累积的数据会越来越大、越来越旧,Relay 不能无限期地在内存中保留所有已获取的数据。为此 Relay 运行一个名为垃圾回收(Garbage Collection)的机制,删除不再被任何组件引用的数据。这本身与"复用缓存"存在张力:数据过早被删除,后续再想复用就得重新等网络请求。好在通常不需要自己操心 GC 与数据保留的配置——应用基础设施会在RelayEnvironment层配置好——但理解它有助于排查缓存复用失效的问题。

查询保留(Query Retention)是控制 GC 的关键:保留一个查询意味着告诉 Relay,该查询及其变量对应的数据不应被删除。多个调用方可以同时保留同一个查询,只要还有至少一个调用方在保留,数据就不会被 GC。默认情况下,使用useQueryLoader/usePreloadedQuery等 API 的查询组件,在挂载期间会保留查询,卸载后即释放,数据随时可能被回收。若想在组件生命周期之外保留数据,可以使用environment.retain()

// 保留查询;这会阻止该查询及其变量的数据被 Relay 垃圾回收 const disposable = environment.retain(queryDescriptor); // 释放 disposable 将释放该查询及变量的数据, // 之后若没有其他方保留,数据随时可能被 GC 删除 disposable.dispose();

控制垃圾回收的两个 Store 配置

Relay Store 提供两个选项来控制 GC 行为,在 RelayModernStore 构造函数 中均有对应字段:

1.gcScheduler—— 决定何时调度一次 GC 执行:

// 示例调度函数:接受一个回调并安排在未来的某个时间执行 function gcScheduler(run: () => void) { resolveImmediate(run); } const store = new Store(source, {gcScheduler});
  • 若不提供,Relay 默认使用resolveImmediate调度 GC(源码见 RelayModernStore.js);
  • 可提供自定义调度函数,让 GC 不那么激进,例如基于时间或 React scheduler 优先级等启发式策略。按约定,实现不应立即执行回调。

2.gcReleaseBufferSize—— 控制 release buffer 大小。Relay Store 内部持有一个释放缓冲区,在查询被原持有者释放后(默认即组件卸载时)仍临时保留指定数量的查询,使返回之前访问过的页面/Tab 时更有可能复用数据:

const store = new Store(source, {gcReleaseBufferSize: 10});
  • 缓冲区大小为 0 等价于没有释放缓冲区,查询会被立即释放并回收;
  • 默认环境下的释放缓冲区大小为 10。

源码中 RelayModernStore.js 使用options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE读取该配置;_pushToReleaseBuffer在缓冲区满时挤出最早的根并调度 GC(RelayModernStore.js)。

数据过期性:显式失效与查询缓存过期时间

假设数据存在于 store 中,还需要考虑其过期性。默认情况下,无论数据在缓存中待了多久,Relay 都不会认为它过期——除非它被显式标记为过期,或者超过了查询缓存过期时间(query cache expiration time)。

全局失效整个 store:调用invalidateStore()会使失效之前写入的所有数据都变为过期状态,下次评估时需要重新获取:

function updater(store) { store.invalidateStore(); }

invalidateStore可以在 mutation、subscription 或本地 store 更新的 updater 中调用。

按记录失效:也可以只失效 store 中的特定记录,只有引用了这些记录的查询会被视为过期:

function updater(store) { const user = store.get('<id>'); if (user != null) { user.invalidateRecord(); } }

订阅失效事件:标记为过期只会在下次评估时触发重新获取。若希望数据失效时立即重新获取(例如当前页面正在展示的数据、或从未卸载的前一个视图的数据),可以用useSubscribeToInvalidationStateHook:

function ProfilePage(props) { const data = usePreloadedQuery( graphql`...`, props.preloadedQuery, ) // 订阅指定用户 ID 的失效状态变化, // 每当该记录被标记为过期时回调触发 useSubscribeToInvalidationState([props.userID], () => { // 在这里可以: // - 传入新的 preloadedQuery 给 usePreloadedQuery 重新评估查询 // - 命令式地重新获取数据 // - 渲染 loading 提示或置灰页面以表示正在刷新 }) return (...); }

查询缓存过期时间:另一个影响"过期"判定的因素是queryCacheExpirationTime。一个查询如果可以用 store 中的记录满足,且满足以下任一条件,则被视为过期:

  • 距上次获取的时间超过了查询缓存过期时间;
  • 其引用的记录中至少有一条被失效过。

该过期检查发生在新的请求发起时(例如调用loadQuery)。引用了过期数据的组件仍能继续渲染这些数据,但任何会被过期数据满足的新请求都会走向网络。配置方式:

const store = new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });

如果不提供该配置,过期检查只会看引用的记录是否被失效过。源码中 getAvailabilityStatus 实现了这一逻辑:当operationFetchTimequeryCacheExpirationTime均存在且operationFetchTime <= Date.now() - queryCacheExpirationTime时返回stale

部分缓存数据下的渲染:renderPolicy 与 Suspense

当使用允许复用缓存的策略(store-or-networkstore-and-network)渲染一个部分缓存的查询时,Relay 支持"部分渲染":立即渲染已缓存的部分,而不是等整个查询全部获取完成。

关键机制在于以 Fragment 作为部分渲染的边界。fragment 组件在其本地声明的数据缺失且正在获取时会挂起(suspend),直到其所属的父查询被获取完成。而 Relay 判定数据缺失时只看本地声明的字段——被 fragment spread 引用的数据缺失不会导致外层查询被判定为缺失。因此,即使某个 fragment 的数据还没到,外层查询已缓存的部分依然可以先行渲染,只需用Suspense包住缺失数据的 fragment 组件即可:

function HomeTab() { const data = usePreloadedQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } `, props.queryRef, ); return ( <> <h1>{data.user?.name}</h1> {/* 用 Suspense 包裹 UsernameComponent, 即使 username 缺失,也可以先渲染 App 的其他部分 */} <Suspense fallback={<LoadingSpinner label="Fetching username" />}> <UsernameComponent user={data.user} /> </Suspense> </> ); }

嵌套 fragment 的处理方式相同:只要某 fragment 所需数据已缓存就能渲染,其子 fragment 数据缺失时用Suspense兜底即可。这种能力可以让我们完全跳过 loading 状态,渲染出更接近最终形态的中间 UI,相关完整示例与讨论见 rendering-partially-cached-data.md。

让不同查询共享缓存:missingFieldHandlers

默认情况下,Relay 只能识别"完全相同的查询"之间的缓存复用:同一个查询获取两次,第二次评估时就知道数据已缓存。但不同查询可能指向同一份数据,例如:

# Query 1 query UserQuery { user(id: 4) { name } } # Query 2 query NodeQuery { node(id: 4) { ... on User { name } } }

这两个查询不同,但引用的是完全相同的数据。Relay 默认不知道node(id: 4)user(id: 4)指向同一对象,需要通过在RelayEnvironment上提供missingFieldHandlers来编码这种等价关系:

const {ROOT_TYPE, Environment} = require('relay-runtime'); const missingFieldHandlers = [ { handle(field, record, argValues): ?string { // 为 node 字段添加处理器 if ( record != null && record.getType() === ROOT_TYPE && field.name === 'node' && argValues.hasOwnProperty('id') ) { return argValues.id } if ( record != null && record.getType() === ROOT_TYPE && field.name === 'user' && argValues.hasOwnProperty('id') ) { // 如果字段是 user(id: $id),按 $id 的值查找记录 return argValues.id; } if ( record != null && record.getType() === ROOT_TYPE && field.name === 'story' && argValues.hasOwnProperty('story_id') ) { // 如果字段是 story(story_id: $story_id),按 story_id 值查找 return argValues.story_id; } return undefined; }, kind: 'linked', }, ]; const environment = new Environment({/*...*/, missingFieldHandlers});

要点:

  • missingFieldHandlers是一个handler 数组,每个 handler 必须包含一个handle函数以及它能处理的缺失字段的kind。主要处理两类字段:
    • 'scalar':包含标量值(数字、字符串等)的字段;
    • 'linked':引用另一个对象(非标量)的字段。
  • handle函数接收缺失的字段、该字段所属的记录、以及本次查询执行中传给该字段的参数:
    • 处理'scalar'字段时,返回一个标量值作为缺失字段的值;
    • 处理'linked'字段时,返回一个ID,指向 store 中应替代该缺失字段的另一个对象。
  • 当 Relay 尝试从本地缓存满足查询并检测到缺失数据时,会在最终判定缺失之前,先运行所有与字段类型匹配的缺失字段处理器。

详细讨论见 filling-in-missing-data.md。从这里可以看到,fetch policy 判定"数据缺失"的最终结果,实际上是 store 检查(environment.check)叠加 missing field handlers 补齐之后的综合结论。

实践建议与完整决策链路

综合以上内容,在实际项目中选择 fetch policy 时可以参考以下思路:

  1. 大多数页面使用默认的store-or-network:能复用缓存就复用,有缺失/过期才回源,兼顾速度与新鲜度;
  2. 需要即时展示且允许后台刷新的场景使用store-and-network:先用缓存秒开,同时后台拉取最新数据覆盖;
  3. 对新鲜度有硬性要求(如支付状态、权限变更)使用network-only:跳过 store 直接走网络,避免展示过期数据;
  4. 纯本地数据(client-only、本地更新产生的数据)使用store-only:永不发网络请求;
  5. 数据保留与 GC:依赖默认的 release buffer(默认 10 条)让返回过的页面快速复用;必要时用environment.retain()延长关键数据的存活时间;需要更长时间保留数据时可通过queryCacheExpirationTimegcScheduler调整策略;
  6. 跨查询复用:当不同查询指向同一数据对象时,配置missingFieldHandlers让 store 检查能识别等价字段,从而提升store-or-network的缓存命中率;
  7. 渲染体验:结合 fragment 边界与Suspense,在部分数据缺失时先渲染已缓存部分,避免整页 loading。

最终,一次loadQuery(..., {fetchPolicy})的完整决策链路可以概括为:

loadQuery 收到 fetchPolicy ├─ store-only:直接返回,不发网络请求 ├─ store-and-network / network-only:必定发起网络请求 └─ store-or-network:environment.check(operation) ├─ available:直接用 store 数据渲染 ├─ stale:视为需要重新获取,发起网络请求 └─ missing:先经 missingFieldHandlers 补齐尝试 └─ 仍缺失 → 发起网络请求

理解这条链路,你就能准确预判每种策略下用户会看到缓存数据、loading 状态还是最新数据,从而把 Relay 的缓存能力真正用起来。

  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载

相关推荐

上一篇:3步彻底解决Windows桌面混乱!NoFences免费开源桌面分区工具完全指南
下一篇:AutoDock Vina终极指南:快速掌握分子对接与虚拟筛选技术

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 16:47:26

Netty 4.1 使用 Protobuf 传输数据:itstack-demo-netty 中级拓展篇二实战解析

文档教程后端 【免费下载链接】CodeGuide :books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总&#xff0c;旨在为大家提供一个清晰详细的学习教程&#xff0c;侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助&#xff0c;请给予支持(关注、…

作者头像 李华