4-5.【状态管理】如何拆分状态以避免过度重渲染?为什么“一个大对象”是性能陷阱?如何设计百万级列表状态?

0 阅读3分钟

在 ArkUI 这种声明式框架中,状态拆分不仅仅是代码整洁度问题,更是直接决定 FPS(每秒帧数) 的性能红线。


1. 为什么“一个大对象”是性能陷阱?

在状态管理中,如果你定义了一个巨型对象 BigModel,即便使用了 @Observed,仍会陷入以下陷阱:

  • 观察者膨胀: 每一个关联该对象的组件都会被注册到这个对象的监听列表中。修改其中一个微小字段,框架需要遍历并通知所有关联组件进行“检查”。
  • 属性冗余校验: 默认情况下,修改 Object.a,框架可能无法确定 Object.b 是否影响了 UI。在没有使用 @Track 优化前,引用了 Object 的所有 UI 节点都会触发 Diff。
  • 内存屏障: 频繁通过 Proxy 修改大对象属性会产生大量的临时闭包,导致 GC(垃圾回收) 频繁触发,造成界面微卡顿。

2. 如何拆分状态以避免过度重渲染?

核心策略是:状态下沉,职责分明。

A. 垂直拆分:按组件职责拆分

不要在父组件管理所有状态。将状态定义在最靠近使用它的子组件中。

  • 反例:Page 里定义所有按钮的 isLoading
  • 正例: 自定义一个 LoadingButton 组件,内部维护自己的 @State isLoading

B. 水平拆分:按变化频率拆分

将“静态/低频变化”的数据与“高频变化”的数据分离。

  • 技巧: 如果一个对象包含 id(不变)、name(低频)和 progress(高频),应将 progress 独立出来,避免因进度更新导致整个 name 组件重绘。

C. 属性过滤:使用 @Track

如果必须使用大对象,请务必给参与 UI 显示的属性加上 @Track

  • 效果: 只有被标记了 @Track 的属性变动才会触发 UI 刷新,未标记的属性仅作为纯数据存储。

3. 如何设计百万级列表状态?

处理“百万级”数据,核心不是如何“存储”,而是如何**“按需渲染”**。

A. 必须使用 LazyForEach

ForEach 会一次性创建所有组件,百万级数据会导致内存直接溢出。LazyForEach 配合 IDataSource 接口,只渲染屏幕可见区域及前后的缓冲项。

B. 状态池化与索引引用

不要在列表项(ListItem)里直接 @Link 整个大型数据对象。

  • 优化方案: 列表项只接收一个 idindex。通过 id 去全局的一个 MapDataCache 中查找数据。
  • 优势: 当某一行数据变化时,只有那一行对应的子组件会收到通知,不会波及整个列表。

C. 避免在组件内做复杂运算

列表滑动时,build() 函数会被高频触发。

  • 原则: 所有的格式化(如日期格式化、金额千分位)应在数据进入列表前处理好,或者使用带缓存的计算属性,绝不要在 Text(this.item.price * 0.8 + '元') 这种地方做计算。

D. 数据预加载与离屏绘制

利用 cachedCount 属性。

  • 设置: LazyForEach(..., { cachedCount: 10 })
  • 作用: 提前渲染不可见区域的 10 个条目,当用户快速滑动时,UI 已经准备好,不会出现白块。

总结:百万级状态架构建议

层面推荐配置目的
存储层序列化数据库 (RDB)避免内存占用过大,支持分页查询。
逻辑层IDataSource 适配器实现数据的“懒加载”逻辑。
组件层@Reusable 装饰器组件复用。销毁的 ListItem 不释放,而是进入回收池等待下次复用,减少内存抖动。
更新层精准 ID 匹配确保修改第 10001 行时,前 10000 行纹丝不动。