尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Relay 数据陈旧性管理:从全局失效、记录级失效到查询缓存过期时间的完整指南

Relay 数据陈旧性管理:从全局失效、记录级失效到查询缓存过期时间的完整指南 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载导读当数据已经存在于 Relay Store 缓存中时如何判断它是否过期、如何主动将某段数据标记为陈旧、以及如何在失效发生时立刻触发重新请求是构建实时、一致的用户界面的关键。本文以 Relay v19 文档 Staleness of Data 为核心骨架结合 relay-runtime 与 react-relay 的源码实现系统讲解 Relay 数据陈旧性的判定规则、全局/局部失效 API、useSubscribeToInvalidationState订阅机制以及queryCacheExpirationTime配置帮助你掌握缓存复用与数据新鲜度之间的平衡术。一、先决条件数据在才会有陈旧一说讨论陈旧stale之前先要确保数据存在present。Relay 的缓存复用模型分为两个层次数据是否存在与数据是否陈旧。前者由数据的驻留retention与垃圾回收Garbage Collection决定后者由本文讨论的失效机制决定。简单回顾 Presence of Data查询首次被获取后只要它还在屏幕上被渲染其数据就会留在 Store 中Relay 通过垃圾回收删除不再被任何组件引用的数据防止内存无限增长组件挂载期间会自动 retain 查询卸载后释放数据可能随时被回收通过environment.retain(queryDescriptor)可以手动保持查询数据不被回收并在不需要时disposable.dispose()释放Store 提供gcScheduler与gcReleaseBufferSize默认 10两个选项控制 GC 策略。在确认数据确实还在缓存中之后下一步才轮到本文的主角——陈旧性判定。二、核心规则默认永不陈旧除非被显式标记Relay 对陈旧性的默认策略非常保守只要数据在 Store 中无论它在缓存里待了多久Relay默认都不会把它视为陈旧除非满足以下两个条件之一数据通过数据失效 API 被显式标记为陈旧例如 Mutation 执行之后数据早于查询缓存过期时间queryCacheExpirationTime。这一规则在 staleness-of-data.md 中有明确表述其核心价值在于缓存命中默认成立数据复用是无忧的只有当你明确知道某段数据已经不再新鲜例如刚刚执行了会改变该数据的 Mutation时才需要主动失效。从源码看这种默认新鲜的语义也贯穿始终RelayModernRecord.js中记录是否陈旧的判定依据是失效时间戳字段INVALIDATED_AT_KEY只有在记录曾被执行过失效操作该字段是 number 类型时才视为陈旧// packages/relay-runtime/store/RelayModernRecord.js (L323-L336) // Returns the epoch at which the record was invalidated, if it const invalidatedAt record[INVALIDATED_AT_KEY]; if (typeof invalidatedAt ! number) { // If the record has never been invalidated, it isnt stale. return ... }这个内部字段在 RelayStoreUtils.js 中被定义为__invalidated_at。也就是说陈旧不是一个模糊的时间概念而是一个显式的、可记录的标志状态。三、全局失效invalidateStore()3.1 用法最粗粒度coarse-grained的失效方式是让整个 Store失效。调用invalidateStore()之后Store 中所有当前缓存的数据都会被标记为陈旧function updater(store) { store.invalidateStore(); }关键行为失效发生时点之前写入 Store 的所有数据都会被标记为陈旧下一次任何查询被求值evaluate时只要它引用的是失效前的数据就会触发重新请求refetchupdater 函数既可以来自 Mutation也可以来自 Subscription甚至可以是 本地数据更新commitLocalUpdate。3.2 源码佐证invalidateStore 的实际落点invalidateStore()并不是凭空实现的它经由多个代理层最终落到RecordSource的标记上。以RelayRecordSourceSelectorProxy为例// packages/relay-runtime/mutations/RelayRecordSourceSelectorProxy.js (L131-L132) invalidateStore(): void { this.__recordSource.invalidateStore(); }在RelayModernStore.js中invalidateStore会被收集进一次Batch{sourceOperations: [], invalidateStore: false}见 L276并在发布publish时随批次一起通知订阅者L282、L481-L482。发布过程中如果批次带invalidateStore: trueStore 会被整体标记失效并广播给所有监听者。这一机制在测试中有非常直观的验证例如 RelayModernEnvironment-CheckWithGlobalInvalidation-test.js 中反复用storeProxy.invalidateStore()配合environment.check()断言全局失效后查询不再命中缓存。3.3 适用场景全局失效适合影响面极大、难以枚举受影响记录的场景例如应用级配置、特性开关feature flags被更新用户权限/身份发生根本性变化后端发生了一次不可追溯的大批量数据变更。它的代价是一刀切——所有缓存都会失效包括那些本可复用的无关数据。因此如果能精确定位受影响的数据应优先考虑下一节的记录级失效。四、记录级失效invalidateRecord()4.1 用法与全局失效相比记录级失效record-level invalidation粒度更细只标记特定的记录record。此时只有引用了这些被失效记录的查询才会被视为陈旧function updater(store) { const user store.get(id); if (user ! null) { user.invalidateRecord(); } }关键行为对被标记的user记录任何缓存中引用了该记录的查询都会变为陈旧下一次被求值时需要重新请求不影响未引用该记录的其他查询——这比全局失效精准得多与invalidateStore()相同updater 同样可以来自 Mutation、Subscription 或本地数据更新。4.2 源码佐证失效时间戳的读写记录级失效的底层实现是把当前时间写入记录的__invalidated_at字段见上文RelayStoreUtils.js中的INVALIDATED_AT_KEY定义。RelayModernRecord.js的invalidateRecord正是基于这个字段实现// packages/relay-runtime/store/RelayModernRecord.js (L323-L336) const invalidatedAt record[INVALIDATED_AT_KEY]; if (typeof invalidatedAt ! number) { // If the record has never been invalidated, it isnt stale. ... }当DataCheckerpackages/relay-runtime/store/DataChecker.js在check阶段判定某条查询是否可用缓存满足时会遍历查询引用的记录只要其中至少一条记录的失效时间戳晚于查询的获取时间该查询就被判定为陈旧需要走网络。这一点在 DataChecker-test.js 中有大量对应用例如user.invalidateRecord()后检查查询不可用。4.3 组合使用一次失效多个记录invalidateRecord()可以在同一个 updater 中多次调用对多个记录逐一标记。配合store.get()的遍历可以构造批量精准失效function updater(store) { const user store.get(user:1); const comment store.get(comment:42); if (user ! null) { user.invalidateRecord(); } if (comment ! null) { comment.invalidateRecord(); } }测试代码如 RelayPublishQueue-test.jsproxy.get(4).invalidateRecord()以及 RelayModernEnvironment-ExecuteMutation-WithLocalInvalidation-test.js 中comment.invalidateRecord()的用例都展示了Mutation 成功后精准失效受影响实体这一典型组合。五、失效后的行为下次求值时自动重新请求理解失效的时机很重要标记为陈旧并不会立即触发重新请求。它的效果是下一次查询被求值时即使是缓存数据也会走网络。例如你从页面 A 导航到页面 B再导航回页面 A——如果页面 A 的查询在离开期间被标记为陈旧那么返回时会重新请求即使数据还在缓存中。这种懒失效lazy invalidation对大多数场景是足够的。但有两个场景下懒失效会导致用户看到陈旧数据当前页面正在展示被失效的数据没有发生导航当前页面的查询不会被重新求值即使数据已陈旧页面依然展示旧内容之前访问过、但从未卸载的视图该视图没有经历卸载/重挂载导航返回时它的查询不会重新求值依旧展示陈旧数据。六、立即响应失效useSubscribeToInvalidationState6.1 用法为了解决上述两个不及时的场景Relay 提供了useSubscribeToInvalidationStatehook当指定 ID 对应的记录被标记为陈旧时立即执行回调而不用等下一次查询求值function ProfilePage(props) { // 查询当前页面对应用户的数据 const data usePreloadedQuery( graphql..., props.preloadedQuery, ); // 订阅指定 userID 的失效状态变化 // 每当该 ID 对应的记录被标记为陈旧时回调立即执行 useSubscribeToInvalidationState([props.userID], () { // 这里可以做 // - 传入新的 preloadedQuery 给 usePreloadedQuery重新求值查询 // - 命令式地重新请求任何数据 // - 渲染 loading spinner 或置灰页面提示正在重新获取 }); return (...); }要点参数是一个ID 数组 一个回调函数数组中任一 ID 对应的记录被标记陈旧回调就会触发回调内部可以反应式地处理例如把preloadedQuery存入 state在回调中 set 一个新值来重新执行顶层的usePreloadedQuery——由于此时查询已陈旧即使数据在缓存中也会重新请求这也是文档中给出的官方推荐做法失效即刷新保持视图实时一致。6.2 源码佐证订阅如何建立与拆除该 hook 的实现位于 useSubscribeToInvalidationState.js核心逻辑非常简洁// packages/react-relay/relay-hooks/useSubscribeToInvalidationState.js (L36-L49) useEffect(() { const invalidationState store.lookupInvalidationState(dataIDs); const disposable store.subscribeToInvalidationState( invalidationState, callback, ); ... return () disposable.dispose(); // 卸载时自动取消订阅 }, [stableDataIDs, callback, environment]);从源码可以看出三点实现细节订阅与组件生命周期绑定通过useEffect建立订阅并在清理函数中dispose()组件卸载后订阅自动拆除不会泄漏ID 数组会被稳定化stableDataIDs即使每次渲染传入新数组字面量内部也会做稳定化处理避免不必要的重复订阅变更自动重建订阅当 ID 集合或回调函数变化时effect 会重新执行订阅随之更新依赖数组[stableDataIDs, callback, environment]。七、查询缓存过期时间queryCacheExpirationTime7.1 概念与判定规则除了显式失效之外Relay 还提供一种基于时间的被动陈旧机制——查询缓存过期时间。它影响的是某个查询query variables 组合能否用 Store 中已有的数据满足。一个查询被认为是陈旧查询stale query当且仅当它满足以下条件之一距上次获取的时间已经超过queryCacheExpirationTime或它引用的记录中至少有一条被失效过即前文讨论的invalidateStore()/invalidateRecord()的影响。重要行为陈旧性检查发生在发起新请求时例如调用loadQuery已经引用陈旧数据的组件仍然可以继续渲染这些数据不会强制踢掉已有 UI但任何依赖陈旧数据才能满足的新请求都会改走网络而不是命中缓存。7.2 配置方式在创建 Store 时传入queryCacheExpirationTime单位毫秒即可const store new Store(source, {queryCacheExpirationTime: 5 * 60 * 1000 });示例中5 * 60 * 1000 5 分钟若不提供该选项则陈旧性检查只看引用的记录是否被失效过即纯显式失效模式该选项同时影响check查询是否可用缓存满足与查询可用性判断两条路径。7.3 源码佐证过期时间的底层判定在 RelayModernStore.js 中queryCacheExpirationTime在构造函数中被存入this._queryCacheExpirationTimeL181随后被用于两处关键判定判定一查询条目root entry是否因超时而陈旧L402-L406const {_queryCacheExpirationTime} this; if ( _queryCacheExpirationTime ! null rootEntry.fetchTime Date.now() - _queryCacheExpirationTime ) { // 该 root 已超过缓存过期时间 - 陈旧 }判定二操作operation variables的可用性判定L891-L921、L1170-L1191// L1189-L1191 if (operationFetchTime ! null queryCacheExpirationTime ! null) { const isStale operationFetchTime Date.now() - queryCacheExpirationTime; if (isStale) { ... } }可以看到判定的本质是比较上次获取时间与当前时间 - 过期时间fetchTime Date.now() - queryCacheExpirationTime即视为过期。这一逻辑简单直接且与记录失效判定是或OR关系——满足任一条件即为陈旧。7.4 设计取舍queryCacheExpirationTime提供的是时间维度的兜底新鲜度保证适合对数据新鲜度有硬性要求、但又不希望为每次变更写显式失效逻辑的场景它与显式失效 API 互补显式失效解决我明确知道数据变了时间过期解决我无法感知数据变了但希望它最多存活这么久实际项目中常两者并用Mutation 成功后显式失效相关记录同时设置一个合理的过期时间作为全局兜底。八、综合实战一个完整的失效即刷新模式结合上述所有机制下面是一个在生产中常见的组合用法——用户资料页在收到资料已更新的信号后立即刷新当前视图import {useState} from react; import {usePreloadedQuery, useSubscribeToInvalidationState} from react-relay; function ProfilePage({preloadedQuery, userID}) { // 将 preloadedQuery 放入 state以便失效后替换触发重新求值 const [query, setQuery] useState(preloadedQuery); const data usePreloadedQuery( graphql query ProfilePageQuery($id: ID!) { user(id: $id) { name; avatar { url } } } , query, ); useSubscribeToInvalidationState([userID], () { // 该用户被 Mutation 失效后立即重新请求 setQuery( // 传入一个新的 preloadedQuery 实例例如通过 loadQuery 重新获取 loadQuery(environment, ProfilePageQuery, {id: userID}), ); }); return (...); }对应的服务端变更侧updater 中精准失效const environment getEnvironment(); commitMutation(environment, { mutation: graphql mutation UpdateUserNameMutation($input: UpdateUserNameInput!) { updateUserName(input: $input) { user { id } } } , variables: {input}, updater: (store) { // 从响应中拿到被修改的 user id精确失效该记录 const user store.get(id); if (user ! null) { user.invalidateRecord(); } }, });这一模式的关键链路是Mutation 写库 → updater 标记记录失效 →useSubscribeToInvalidationState收到失效事件 → 替换 preloadedQuery 触发重新请求。整套流程无需导航即可实现失效即刷新。九、机制全景与选型建议最后将 Relay 的陈旧性管理机制汇总如下机制粒度触发方式生效时机适用场景invalidateStore()整个 Storeupdater 中显式调用下次查询求值时影响面大、难以枚举的全局变更权限、配置invalidateRecord()单条记录updater 中显式调用下次查询求值时Mutation 后精准失效受影响实体useSubscribeToInvalidationState指定 ID 集合记录失效时立即回调立即执行当前页/已挂载视图需要失效即刷新queryCacheExpirationTime查询queryvariables基于时间的被动判定发起新请求时对新鲜度有硬性时间要求的兜底机制选型建议能精确就精确优先invalidateRecord()避免全局失效导致无关缓存被无谓重取能立即就立即失效数据若正被当前视图展示配合useSubscribeToInvalidationState实现即时刷新避免陈旧 UI 短暂可见时间兜底不可少为对新鲜度敏感的数据设置合理的queryCacheExpirationTime作为显式失效机制之外的保险丝。延伸阅读缓存数据的存在性、retention 与垃圾回收Presence of DataMutation、Subscription 与本地更新中的 updaterGraphQL Mutations、GraphQL Subscriptions、Local Data Updates核心源码RelayModernStore.jsqueryCacheExpirationTime判定、invalidateStore批次发布RelayRecordSourceSelectorProxy.jsupdater 中invalidateStore的入口RelayModernRecord.js__invalidated_at失效时间戳读写useSubscribeToInvalidationState.js订阅 hook 实现相关测试用例RelayModernEnvironment-CheckWithGlobalInvalidation-test.js、RelayModernEnvironment-CheckWithLocalInvalidation-test.js、DataChecker-test.js赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 15 数据陈旧性Staleness of Data完全指南store 失效、记录失效与查询缓存过期时间Relay 15 数据陈旧性Staleness of Data完全指南store 失效、记录失效与查询缓存过期时间 本指南以 Relay v15 官方文档前端开发工具Relay 数据过期Staleness of Data机制详解全局失效、记录级失效与查询缓存过期Relay 数据过期Staleness of Data机制详解全局失效、记录级失效与查询缓存过期 导读 在 Relay 应用中数据存在于 Store前端开发工具Relay 数据过期与失效处理指南Store 失效 API 与查询缓存过期时间详解Relay 数据过期与失效处理指南Store 失效 API 与查询缓存过期时间详解 数据存在于 Store 中并不等于数据是最新的 。在 Relay 构建的数前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表