在 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 整个大型数据对象。
- 优化方案: 列表项只接收一个
id或index。通过id去全局的一个Map或DataCache中查找数据。 - 优势: 当某一行数据变化时,只有那一行对应的子组件会收到通知,不会波及整个列表。
C. 避免在组件内做复杂运算
列表滑动时,build() 函数会被高频触发。
- 原则: 所有的格式化(如日期格式化、金额千分位)应在数据进入列表前处理好,或者使用带缓存的计算属性,绝不要在
Text(this.item.price * 0.8 + '元')这种地方做计算。
D. 数据预加载与离屏绘制
利用 cachedCount 属性。
- 设置:
LazyForEach(..., { cachedCount: 10 })。 - 作用: 提前渲染不可见区域的 10 个条目,当用户快速滑动时,UI 已经准备好,不会出现白块。
总结:百万级状态架构建议
| 层面 | 推荐配置 | 目的 |
|---|---|---|
| 存储层 | 序列化数据库 (RDB) | 避免内存占用过大,支持分页查询。 |
| 逻辑层 | IDataSource 适配器 | 实现数据的“懒加载”逻辑。 |
| 组件层 | @Reusable 装饰器 | 组件复用。销毁的 ListItem 不释放,而是进入回收池等待下次复用,减少内存抖动。 |
| 更新层 | 精准 ID 匹配 | 确保修改第 10001 行时,前 10000 行纹丝不动。 |