一、状态复用概述

什么是状态复用

状态复用,说白了就是「把已经算好的结果存起来,下次直接用」。

举个例子。你写了一个搜索框,用户每敲一个字就发请求。如果每次都重新查数据库,那服务器早崩了。我习惯的做法是:把最近 10 次搜索结果缓存到内存里,用户再搜同样的关键词,直接返回缓存数据。这就是状态复用最朴素的形式。

在实时更新场景下,状态复用要解决的问题更复杂一些。它不只是缓存,还包括:

  • 跨组件共享:多个组件需要同一份数据,不用各自拉取
  • 跨页面保持:用户从列表页跳到详情页再回来,列表状态还在
  • 跨会话延续:用户刷新页面甚至关掉浏览器再打开,某些状态还能恢复

核心定义:状态复用是指在应用的不同生命周期阶段(组件挂载/卸载、页面切换、会话重启),通过合理的存储和恢复策略,避免重复计算或重复请求,从而提升性能和用户体验。

为什么需要状态复用

我刚开始做前端那会儿,觉得状态管理就是 Redux 那一套。后来在实时数据项目里踩了坑,才明白事情没那么简单。

给你列几个真实场景:

场景 不复用的后果 复用后的收益
用户频繁切换 Tab 每次切回来都重新请求,白屏等待 瞬间展示,零等待
列表页 → 详情页 → 返回列表 滚动位置丢失,数据重新加载 滚动位置保留,数据秒开
WebSocket 推送频繁 每次推送都全量更新 DOM 只更新变化的部分
用户填写长表单后误刷新 所有填写内容丢失 自动恢复,用户不感知

你看,状态复用不只是「优化」,它直接决定了用户觉得你的应用「卡不卡」、「好不好用」。我记得有一次做股票行情看板,每秒推送几十条数据。如果不做状态复用,页面直接卡死。后来用了增量更新 + 缓存池,CPU 占用从 80% 降到了 15%。

实时更新场景的挑战

实时更新场景下做状态复用,跟普通场景完全不是一个量级。我总结了几大难点:

挑战一:数据一致性

实时数据一直在变。你缓存了一份数据,下一秒服务器告诉你「那条数据已经变了」。这时候你是更新缓存,还是丢弃缓存重新拉?

我曾经在一个在线协作编辑项目里遇到这个问题。两个人同时编辑同一个文档,A 的本地缓存是「版本 10」,B 提交了「版本 11」。如果 A 的缓存不更新,他看到的永远是旧数据。但如果每次推送都全量更新缓存,性能又扛不住。

我的经验:对于实时数据,不要全量缓存。只缓存「不变的部分」,比如用户信息、配置项。实时变化的数据用「订阅 + 增量更新」策略,每次只推送变化字段。

挑战二:内存管理

实时应用跑得越久,积累的状态越多。如果不做清理,内存迟早爆掉。

想想看:一个聊天应用,用户打开了 50 个群聊。每个群聊的历史消息都缓存在内存里。手机内存才多大?

  • LRU 淘汰:最近最少使用的缓存优先清除
  • TTL 过期:超过一定时间的数据自动失效
  • 按场景分级:当前可见的页面用强引用,不可见的用弱引用

注意:不要为了复用而无限缓存。我曾经见过一个项目,缓存了所有历史数据,结果页面打开 30 分钟后内存占用超过 500MB。用户手机直接闪退。后来加了 LRU 限制,内存稳定在 80MB 以内。

挑战三:状态同步时机

实时数据什么时候该同步?什么时候该用缓存?这个时机很难把握。

举个例子:

  • 用户正在看股票 K 线图,每秒都有新数据进来。你是每 1 秒刷新一次界面,还是攒够 10 条再批量更新?
  • 用户切换到后台看别的页面,这时候数据还要不要继续拉?
  • 用户切回来,是直接用缓存,还是先发请求确认数据是最新的?

这些问题没有标准答案。我个人的做法是:根据数据的重要性和实时性要求,给每条数据打标签。高实时性的数据(如股票价格)走 WebSocket 推送,低实时性的数据(如用户资料)走缓存 + 定期刷新。

挑战四:跨组件/跨页面状态共享

实时应用里,同一个数据可能被多个组件、多个页面同时使用。比如用户信息,导航栏要用、个人中心要用、设置页面也要用。

如果每个组件都自己拉一份,不仅浪费请求,还会出现数据不一致——导航栏显示「张三」,个人中心显示「张三(已改名)」。这就尴尬了。

我建议用「单一数据源」模式:

// 单一数据源示例
const userStore = {
  data: null,
  subscribers: new Set(),

  // 订阅数据变化
  subscribe(callback) {
    this.subscribers.add(callback);
    return () => this.subscribers.delete(callback);
  },

  // 更新数据并通知所有订阅者
  setData(newData) {
    this.data = newData;
    this.subscribers.forEach(cb => cb(newData));
  }
};

这样所有组件都从同一个地方拿数据,数据变了大家一起更新。不会出现「一个改了另一个不知道」的情况。

本章知识体系

说了这么多,我把本章的核心逻辑画成了一张图,方便你理解:

状态复用核心逻辑 状态复用 为什么需要? 复用什么东西? 怎么复用? 避免重复请求 提升用户体验 组件状态 请求结果 缓存策略 状态管理 实时更新场景的四大挑战 数据一致性 内存管理 同步时机 跨组件共享

这张图把本章的核心脉络串起来了。你从上往下看:先搞清楚「为什么需要状态复用」,再明确「复用什么东西」,最后思考「怎么复用」。而实时更新场景给这三个环节都带来了额外的挑战。

嗯,这一章算是开个头。后面的章节会逐个深入这些挑战,给出具体的解决方案和代码实现。你先把基础概念理清楚,后面学起来会轻松很多。