触发条件

  • 页面数据查询依赖 userPosition 对象。
  • 应用根组件、页面组件和定位展示组件各自发起浏览器定位。
  • 定位展示组件随 Tab 条件渲染反复挂载,并在挂载时绕过已有定位缓存。
  • 每次定位完成都创建新的位置对象;GPS 轻微抖动也会被广播为位置变化。
  • 数据加载把“校区静态数据”和“依赖位置的周边数据”绑在同一个全页 loading 生命周期中。

症状

  • 页面数据已经显示,随后突然重新出现整页 loading。
  • 同一校区的静态接口在一次进入过程中请求两到三次。
  • 切换 Tab 后是否复现不稳定:GPS 返回值完全相同时可能不触发,坐标轻微变化时触发。
  • 逆地理缓存明明命中,POI、精选和周边列表仍重复请求。

容易误判的方向

  • 只检查逆地理接口的 TTL。逆地理结果缓存不等于位置采集缓存,也不等于业务列表缓存。
  • 只优化后端接口延迟。服务端即使只有毫秒级,公网多次传输较大的 JSON 加上全页 loading 仍然明显。
  • 只用值相等判断抑制广播。多个调用方仍可能直接 setUserPosition(newObject),触发依赖对象引用的 Effect。
  • 把问题归因于 React Strict Mode。生产环境也会因真实的多调用点、组件重挂载和定位回调时序复现。

根因

这是三个独立生命周期被错误耦合后的结果:

  1. 位置采集:多个组件并发调用 getCurrentPosition(),但没有共享进行中的 Promise。
  2. 位置状态传播:每个完成回调都写状态或广播事件,新对象引用和 GPS 抖动均可触发更新。
  3. 业务数据加载:userPosition 一变化,就重新加载所有校区数据,并把已有内容替换成全页 loading。

最终解决原则

  1. 为完整定位链路建立单飞 (Singleflight):学校桥、原生桥和浏览器 Geolocation 共用一个进行中的 Promise。
  2. 自动定位统一走 ensureLocation():先读 TTL 缓存,只有过期或用户明确点击“重新定位”时才强制采集。
  3. 位置状态使用稳定的标量查询键,例如坐标网格加坐标系,而不是直接依赖对象引用。
  4. 对位置变化设置有意义的距离阈值;只有跨出网格或移动超过阈值才刷新周边结果。
  5. 拆分查询:校区、校内 POI、精选按校区缓存;周边列表才依赖位置键;逆地理使用自己的位置键。
  6. 后台刷新保留旧数据,只显示局部刷新状态;首次无数据才使用整页 loading。
  7. 为请求增加取消或序列号保护,避免旧请求晚到覆盖新请求或提前关闭 loading。

验证方法

  • 清空定位缓存,模拟延迟 1–2 秒的 Geolocation,记录 UI 状态和请求时间线。
  • 执行“首页 → 周边 → 首页”,分别测试相同坐标和轻微抖动坐标。
  • 断言同一校区的静态接口首次只请求一次,切 Tab 不再请求。
  • 断言定位到达后只刷新依赖位置的查询,已有内容不被整页 loading 替换。
  • 检查响应头和浏览器网络面板,区分应用内缓存、HTTP 缓存和 CDN 缓存。

识别口诀

“数据先出现、定位后转圈”时,优先检查:谁在定位、谁在广播、谁把位置变化绑定成了全量刷新。


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend