尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Relay 19 缓存数据复用完全指南:Fetch Policy、垃圾回收、数据失效与部分渲染

发布时间:2026/9/23 17:49:25

资讯中心
01
ARTICLE

Relay 19 缓存数据复用完全指南:Fetch Policy、垃圾回收、数据失效与部分渲染

Relay 19 缓存数据复用完全指南:Fetch Policy、垃圾回收、数据失效与部分渲染
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本文基于 Relayrelay29/relayv19.0.0 官方文档的 Reusing Cached Data 章节introduction.md展开完整讲解 Relay 如何复用本地 Store 中已经缓存的数据通过fetchPolicy控制查询的读取与网络请求策略通过保留retain与垃圾回收GC延长缓存生命周期通过失效 API 与缓存过期时间管理数据时效通过missingFieldHandlers补齐跨查询的数据关联并通过 Suspense 边界渲染部分缓存数据。读完本文你将掌握一套从命中缓存到兜底网络再到按需失效的完整缓存复用实战方案并了解其底层实现依据。为什么需要复用本地缓存数据应用运行过程中Relay 会不断累积并缓存多次查询获取的数据。这些数据在内存 Store 中会保留一段时间而很多时候我们希望直接复用并立即渲染本地已缓存的数据而不是每次满足查询时都等待一次网络请求。官方章节的引言introduction.md给出了两个典型场景应用内 Tab 切换每个 Tab 渲染一个查询。若某个 Tab 已被访问过再次访问时应能立即渲染无需等待网络请求重新获取已取过的数据。从 Feed 进入帖子详情页帖子已在 Feed 中渲染过进入其 permalink 页面时应立即渲染。即便详情页需要的字段比 Feed 多也应尽可能复用本地已有的数据、立即渲染可用部分而不因缺少一小部分数据而阻塞整个页面的渲染。围绕这两个目标整个章节由五个子主题组成本文按此脉络逐层展开fetch-policies.md如何通过fetchPolicy控制缓存读取与网络请求presence-of-data.md数据在 Store 中的存在性与生命周期垃圾回收、保留机制staleness-of-data.md数据的时效性失效 API、查询缓存过期时间filling-in-missing-data.md用missingFieldHandlers补齐跨查询数据关联rendering-partially-cached-data.md以 Fragment 为边界渲染部分缓存数据。一、Fetch Policy控制缓存读取与网络请求复用本地缓存的第一步是给loadQuery传入fetchPolicy。loadQuery通常由useQueryLoader提供相关用法见 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 中的可用性是否发起网络请求。四种 Fetch Policy 详解默认情况下Relay 会先尝试从本地缓存读取查询只要该查询有任何数据缺失或过期就会从网络拉取整个查询这个默认策略被称为store-or-network。FetchPolicy复用本地缓存发起网络请求说明store-or-network默认会仅当数据缺失或过期时查询被完整缓存则不发起请求store-and-network会总是无论数据是否缺失/过期始终请求network-only不会总是忽略本地缓存及其存在性/时效性store-only只会从不取数责任交由调用方也可用于读取完全本地client-only的数据另外Fetching and Rendering Different Datarefetch 中讨论的refetch函数同样接受fetchPolicy可用于分页、重取不同变量等场景。源码印证Fetch Policy 的类型定义与判定逻辑从源码看这四种策略是 Relay 运行时的一等类型。在 RelayRuntimeTypes.js 中export type FetchQueryFetchPolicy store-or-network | network-only; export type FetchPolicy FetchQueryFetchPolicy | store-and-network | store-only;而fetchQuery的底层实现fetchQuery.js展示了store-or-network的具体判定先调用environment.check(operation)检查查询在 Store 中的可用性仅当queryAvailability.status ! available时才走网络case store-or-network: { const queryAvailability environment.check(operation); const shouldFetch queryAvailability.status ! available; let observable; if (!shouldFetch) { observable RelayObservable.fromSnapshot( environment.lookup(operation.fragment), ).map(readData); } else { observable getNetworkObservable(environment, operation).map(readData); } ... }需要特别提醒独立的fetchQuery函数fetchQuery.js默认fetchPolicy是network-onlyoptions?.fetchPolicy ?? network-only与 hooks 体系下loadQuery的默认store-or-network不同。这与fetchQuery面向一次性取数的语义一致——它始终走网络、始终写回缓存但不读取缓存若要用它复用缓存需显式传入store-or-network。environment.check的底层判定在 RelayModernStore.js 的getAvailabilityStatus中实现输出三种状态之一available可直接从缓存渲染、missing数据缺失、stale数据过期。这也是缺失或过期即走网络策略的核心依据。二、数据的存在性垃圾回收与查询保留要复用缓存前提是数据还在 Store 里。一个关键认知是某个查询的数据通常在首次被获取后、且该查询正在屏幕渲染期间存在于 Store 中从未取过的查询自然缺失。但应用无法无限期地在内存中保留所有取过的数据——数据会越来越多、越来越旧。为此 Relay 会运行一个名为Garbage Collection垃圾回收的进程删除不再被任何组件引用的数据。查询保留Query Retention保留retain一个查询等于告知 Relay该查询及其变量的数据不应被垃圾回收。一个查询可被多个调用方同时保留只要至少有一个调用方在保留它它就不会被删除。默认情况下使用useQueryLoader/usePreloadedQuery等 API 的查询组件在挂载期间会保留查询卸载后即释放此后数据随时可能被 GC 删除。若需在组件生命周期之外保留查询可使用retain操作完整示例见 Retaining Queries// 保留查询防止该查询及其变量的数据被 Relay 垃圾回收 const disposable environment.retain(queryDescriptor); // dispose 释放该查询及其变量的数据 // 若未被其他地方保留则随时可能被垃圾回收删除 disposable.dispose();这使得组件卸载后其他组件或同一组件的未来实例仍可复用被保留的数据。从实现看retain在 RelayModernStore.js 中通过引用计数实现首次 retain 时创建rootEntryrefCount: 1再次 retain 递增dispose时递减当refCount归零后若查询尚未超过缓存过期时间则被推入 release buffer见下文否则直接从_roots中删除并调度 GCconst rootEntry this._roots.get(id); if (rootEntry ! null) { if (rootEntry.refCount 0) { // 该条目本应在 release buffer 中被重新 retain 后移出 this._releaseBuffer this._releaseBuffer.filter(_id _id ! id); } rootEntry.refCount 1; } else { this._roots.set(id, {operation, refCount: 1, epoch: null, ...}); }控制垃圾回收的两个 Store 选项官方文档指出通常你无需关心 GC 与数据保留的配置它应在RelayEnvironment层的应用基础设施中配置此处仅为参考。目前可给 Relay Store 提供两个选项GC SchedulerGC 调度器gcScheduler是提供给 Store 的一个函数决定何时调度执行一次 GC// 示例调度函数接受一个回调并在未来某个时间运行它 function gcScheduler(run: () void) { resolveImmediate(run); } const store new Store(source, {gcScheduler});默认未提供gcScheduler时Relay 使用resolveImmediate调度 GC可提供自定义调度器降低 GC 的激进程度例如基于时间、React scheduler 优先级或其他启发式策略按惯例实现不应立即同步执行回调。源码中Store 构造器将gcScheduler存为this._gcScheduler默认值为resolveImmediateRelayModernStore.js并在 GC 触发点通过this._gcScheduler(this._gcStep)调度RelayModernStore.js。GC Release Buffer Size释放缓冲区大小Relay Store 内部持有一个 release buffer用于在被释放例如渲染该查询的组件卸载后将一定数量可配置的查询临时继续保留。这使得返回之前访问过的页面、Tab 或内容时缓存复用更有可能成功。const store new Store(source, {gcReleaseBufferSize: 10});缓冲区大小为 0等价于没有 release buffer查询被释放后立即被回收默认缓冲大小为 10。源码中对应常量DEFAULT_RELEASE_BUFFER_SIZE 10RelayModernStore.jsStore 通过_pushToReleaseBuffer与_releaseBuffer.length this._gcReleaseBufferSize的判断实现 FIFO 淘汰RelayModernStore.js缓冲已满时最旧的条目被挤出随后进入可回收状态。三、数据的时效性失效机制与查询缓存过期时间数据存在于 Store 中还不够还需考虑其时效性。默认情况下Relay 不会因为数据在缓存中待得久就认为其过期除非它被显式标记为 stale通过数据失效 API或已超过查询缓存过期时间。标记数据过期非常有用例如在执行 Mutation 后我们明确知道某些数据不再新鲜。Relay 在 Store 更新中暴露了以下失效 API。全局失效invalidateStore()最粗粒度的失效是使整个 Store失效所有已缓存数据在失效后都被视为过期。可在 updater 函数中调用function updater(store) { store.invalidateStore(); }调用后所有在失效发生前写入 Store 的数据都被视为过期任何查询在下一次被求值evaluate时都需重新请求updater 函数可以来自 mutation、subscription 或本地 Store 更新。实现上invalidateStore在 RelayRecordSourceProxy.js 中定义并经由RelayRecordSourceSelectorProxy即 updater 收到的store暴露RelayRecordSourceSelectorProxy.js。定向失效invalidateRecord()也可以更精确地只失效特定记录。与全局失效不同只有引用了被失效记录的查询才会被视为过期function updater(store) { const user store.get(id); if (user ! null) { user.invalidateRecord(); } }对user记录调用invalidateRecord()后该用户被标记为 stale任何缓存中引用了该用户的查询都被视为过期下次求值时需重新请求。实现位于 RelayRecordProxy.js 的invalidateRecord()方法。订阅失效状态useSubscribeToInvalidationState仅将 Store 或记录标记为过期会在查询下次被求值例如再次导航回渲染该过期查询的页面时触发重新请求。但对以下场景是不够的失效的数据已在当前页面可见由于没有导航发生当前页面的查询不会被重新求值用户会继续看到过期数据失效的数据渲染在从未卸载的上一视图中回退导航时该视图的查询不会被重新求值同样会显示过期数据。为支持这些场景Relay 提供了useSubscribeToInvalidationStatehook类型声明见 useSubscribeToInvalidationState.d.tsfunction ProfilePage(props) { // 示例为当前页面的给定用户查询数据 const data usePreloadedQuery( graphql..., props.preloadedQuery, ) // 订阅给定用户 ID 的失效状态变化 // 每当该 ID 对应的记录被标记为过期回调即被触发 useSubscribeToInvalidationState([props.userID], () { // 这里可以 // - 通过向 usePreloadedQuery 传入新的 preloadedQuery 重新求值查询 // - 命令式地重新获取数据 // - 渲染 loading spinner 或灰化页面以提示正在重新获取 }) return (...); }该 hook 接收一个 id 数组和一个回调数组中任一 id 对应记录被标记为过期时回调触发回调内可响应并重新获取/更新正在渲染过期数据的当前视图。例如把preloadedQuery保存在 state 中并在此处设置新值以重执行顶层usePreloadedQuery——由于查询此时已过期即便数据在缓存中也会被重新请求。从实现看useSubscribeToInvalidationState.js该 hook 通过useRelayEnvironment()获取环境经store.lookupInvalidationState(dataIDs)取得失效状态对象再调用store.subscribeToInvalidationState(invalidationState, callback)建立订阅并在 id 集合或回调变化时重建订阅、卸载时自动销毁。查询缓存过期时间Query Cache Expiration Time查询缓存过期时间决定了某个操作一个查询及其变量组合能否用 Store 中已有数据满足即该查询的数据是否已过期。一个查询被认为是过期查询stale query当它可以用 Store 中的记录满足并且满足以下任一条件距上次获取的时间超过查询缓存过期时间或引用了至少一条被失效的记录。过期检查发生在发起新请求时例如调用loadQuery。引用过期数据的组件仍能继续渲染这些数据但任何本可用过期数据满足的新请求都会改走网络。配置方式是把queryCacheExpirationTime传给 Relay Storeconst store new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });若未提供该选项过期检查将只依据所引用记录是否被失效。在 RelayModernStore.js 的getAvailabilityStatus中两种过期来源被统一处理只要操作引用的记录存在mostRecentlyInvalidatedAt最近失效时间且晚于操作的最后写入时间即判定stale否则再比较operationFetchTime与Date.now() - queryCacheExpirationTime。这也印证了文档中记录失效与缓存过期时间两条 stale 判定路径。四、填充缺失数据Missing Field Handlers前文讨论了完整或部分缓存数据的复用但仍存在一种情况不同查询指向同一份数据而 Relay 默认无法感知这种等价关系。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提供missingFieldHandlersconst {ROOT_TYPE, Environment} require(relay-runtime); const missingFieldHandlers [ { handle(field, record, argValues): ?string { // 务必为 node 字段添加 handler 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函数接收三个参数缺失的field、该字段所属的record以及查询当前执行时传给该字段的argValues参数值处理scalar字段时handle应返回一个标量值用作缺失字段的值处理linked字段时handle应返回一个ID指向 Store 中应替代该缺失字段的另一个对象。Relay 在从本地缓存尝试满足查询时每当检测到缺失数据会先运行与字段类型匹配的缺失字段 handler再最终判定数据是否缺失。值得补充的是源码中的MissingFieldHandler类型实际支持三种 kindRelayStoreTypes.jsscalar、linked以及pluralLinked处理引用对象数组的列表字段其中pluralLinked的handle返回?DataID数组export type MissingFieldHandler | { kind: scalar, handle: ( field: NormalizationScalarField, parentRecord: ?ReadOnlyRecordProxy, args: Variables, store: ReadOnlyRecordSourceProxy, ) unknown, } | { kind: linked, handle: (...) ?DataID, } | { kind: pluralLinked, handle: (...) ?ReadonlyArrayDataID, };同时环境在构造时会对该配置做兜底处理this._missingFieldHandlers config.missingFieldHandlers ?? [];RelayModernEnvironment.js。相关行为在 RelayModernEnvironment-ExecuteWithDeferWithinModule-test.js 等测试中有覆盖验证。五、渲染部分缓存的数据以 Fragment 为边界部分渲染partial rendering指立即渲染一个部分缓存的查询——查询的一部分数据缺失、另一部分已缓存我们希望立即渲染已缓存的部分而不必等整个查询取回。这在尽快渲染页面、已知部分数据已缓存、可跳过 loading 状态的场景中非常有用。例如个人资料页用户名字极可能在应用使用过程中已被缓存访问资料页时若名字已缓存就应立即渲染即使资料页其余数据尚未就绪。Fragment 作为部分渲染的边界部分渲染依赖 Fragment 组件的suspend挂起能力参见 Loading States with Suspense。一个 Fragment 组件在渲染时若其本地声明的任何数据缺失且正在获取就会挂起——直到其所属查询父查询取回数据。假设有如下 Fragment 组件/** * UsernameComponent.react.js * * Fragment Component */ import type {UsernameComponent_user$key} from UsernameComponent_user.graphql; const React require(React); const {graphql, useFragment} require(react-relay); type Props { user: UsernameComponent_user$key, }; function UsernameComponent(props: Props) { const user useFragment( graphql fragment UsernameComponent_user on User { username } , props.user, ); return (...); } module.exports UsernameComponent;以及如下查询组件——它查询部分数据并内嵌了上面的 Fragment/** * AppTabs.react.js * * Query Loader Component */ // .... const onSelectHomeTab () { loadHomeTabQuery({id: 4}, {fetchPolicy: store-or-network}); } // ... /** * HomeTab.react.js * * Query Component */ const React require(React); const {graphql, usePreloadedQuery} require(react-relay); const UsernameComponent require(./UsernameComponent.react); function HomeTab(props: Props) { const data usePreloadedQuery( graphql query HomeTabQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 UsernameComponent user{data.user} / / ); }假设渲染HomeTab时之前只取过User{id: 4}的name且它已在当前 Relay 环境的 Store 中缓存。若以允许复用本地缓存的fetchPolicystore-or-network或store-and-network渲染该查询会发生如下过程查询检查自身本地直接声明的数据是否缺失。这里没有缺失查询只直接选取name字段且它在 Store 中可用。Relay 只把本地声明且缺失的数据视为缺失也就是说Fragment spread 内选取的数据不影响外层查询/Fragment 是否被判定为缺失。查询没有缺失数据于是渲染然后尝试渲染子组件UsernameComponent。UsernameComponent渲染UsernameComponent_userFragment 时Relay 发现所需数据缺失——具体是username缺失。此时UsernameComponent会挂起直到网络请求完成。注意无论选择哪种 fetchPolicy只要整个查询含 Fragment有任何数据缺失就总会启动一次网络请求。这时由于username缺失导致UsernameComponent挂起理想情况下仍应立即可渲染已缓存的name。但由于没有用Suspense捕获该 Fragment 的挂起挂起会向上冒泡导致整个App挂起。要达到name可用即渲染、即使username缺失的效果只需把UsernameComponent包进Suspense让App的其他部分继续渲染/** * HomeTab.react.js * * Query Component */ const React require(React); const {Suspense} require(React); const {graphql, usePreloadedQuery} require(react-relay); const UsernameComponent require(./UsernameComponent.react); function HomeTab() { const data usePreloadedQuery( graphql query AppQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } , props.queryRef, ); return ( h1{data.user?.name}/h1 {/* 把 UserComponent 包进 Suspense允许 App 的其他部分 在 username 缺失时仍然渲染。 */} Suspense fallback{LoadingSpinner labelFetching username /} UsernameComponent user{data.user} / /Suspense / ); }上述机制对嵌套 FragmentFragment 内包含其他 Fragment同样成立若渲染某 Fragment 所需数据已缓存该 Fragment 组件就能渲染无论其子/后代 Fragment 的数据是否缺失若子 Fragment 数据缺失将其包在Suspense中即可让其他 Fragment 和应用其余部分继续渲染。正如引言中的动机示例所述这正是我们想要的跳过 loading 状态渲染更接近最终状态的中间 UI。相关的 suspend/读取行为在 RelayReader.js 与 hooks 的 FragmentResource如 useFragmentInternal.js中实现Fragment 组件读取数据时若发现缺失会进入挂起等待父查询完成。结语缓存复用五步走把整个章节串起来一套完整的缓存复用方案可以归纳为选择策略通过loadQuery的fetchPolicystore-or-network/store-and-network/network-only/store-only决定读不读缓存、发不发请求保障存在性理解 GC 与保留机制必要时用environment.retain()延长数据生命周期或调大gcReleaseBufferSize默认 10配合返回导航复用管理时效性在 mutation/subscription/本地更新的 updater 中按需调用store.invalidateStore()或record.invalidateRecord()需要即时刷新时用useSubscribeToInvalidationState订阅并可通过queryCacheExpirationTime配置缓存过期阈值打通跨查询关联为跨查询的等价数据如node(id)与user(id)配置missingFieldHandlers让 Relay 在缓存判定时看穿字段差异渲染部分缓存利用 Fragment 的 suspend 语义用Suspense边界让已缓存部分立即渲染、缺失部分异步补全。延伸阅读本章节全部子文档reusing-cached-data 目录查询加载与渲染Queries、Loading States with Suspense手动保留查询Retaining Queries失效与更新GraphQL Mutations、GraphQL Subscriptions、Local Data Updates核心实现RelayModernStore.js、RelayModernEnvironment.js、RelayStoreTypes.js、fetchQuery.js、useSubscribeToInvalidationState.js赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 缓存数据复用实战指南Fetch Policy、数据可用性与垃圾回收全解析Relay 缓存数据复用实战指南Fetch Policy、数据可用性与垃圾回收全解析 在 Relay 驱动的数据型 React 应用中随着用户不断使用Re前端开发工具Relay 缓存数据复用完全指南Fetch Policy、数据保留与局部渲染实战Relay 缓存数据复用完全指南Fetch Policy、数据保留与局部渲染实战 缓存复用是 Relay 数据获取体系中最能直接影响用户体验的一环应用运行期前端开发工具Relay 缓存数据复用指南Fetch Policy、数据保留与失效机制全解析Relay 缓存数据复用指南Fetch Policy、数据保留与失效机制全解析 导读 在长时间运行的应用中Relay 会将在内存 store 中累积并缓存前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。