4-3.【状态管理】当 @State 变量发生变化时,底层发生了什么?是否每次都会全量重建?是否存在 Diff 算法?

41 阅读3分钟

这是一个关于 ArkUI 渲染引擎底层机制的核心问题。简单来说,ArkUI 的设计目标就是避开全量重建

1. 底层:观察者模式与依赖收集

当你定义一个 @State 变量时,ArkTS 编译器会进行“代码注入”。原本的简单变量被包装成了一个状态管理对象

  • Setter 拦截: 当你执行 this.count++ 时,实际上触发了该对象的 Setter 拦截器。
  • 通知链触发: Setter 会立即通知与之绑定的 观察者(Subscriber) 。在 ArkUI 中,这些观察者就是那些在 build() 函数中引用了该变量的 UI 组件节点

2. 是否每次都会全量重建?(绝对不会)

ArkUI 采用的是局部刷新机制。它不会重新执行整个 build() 函数,也不会重建整个组件树。

  • 精确到组件(Component Level): 框架记录了每个状态变量被哪些具体的组件(如 TextImageButton)所使用。
  • 指令级更新: 当状态变化时,框架仅会找到受影响的那个组件,并调用该组件对应的底层 C++ 渲染节点的更新接口。例如,修改 Text 的内容,底层只会触发 setText 操作,而不会销毁再创建一个新的 Text 节点。

3. 这里的“Diff 算法”与 React 有什么区别?

这是 ArkUI 最具特色的地方。它并不像 React 那样维护一棵完整的“虚拟 DOM 树”并在内存中进行全量 Diff。

ArkUI 的“微观 Diff”:

  1. 静态分析: 在编译阶段,ArkTS 已经分析出了哪些 UI 结构是静态的(永远不会变),哪些是动态的(绑定了状态)。

  2. 属性 Diff: 当状态变化触发组件更新时,框架只对该组件的属性进行 Diff。例如,如果 fontSize 变了但 color 没变,底层渲染引擎只会更新字号属性。

  3. 列表 Diff(ForEachLazyForEach): * ForEach:如果 ID 没变,框架会尝试复用组件,但如果列表很长,性能压力依然很大。

    • LazyForEach:这是 ArkUI 的性能杀手锏。它结合了数据懒加载键值 Diff(Key-based Diff) 。只有在屏幕可见范围内的节点才会被创建,且只有 ID 发生变化的项才会触发重建。

4. 渲染全流程图解

  1. 状态变更: this.value = newValue
  2. 触发拦截: 状态管理模块感知变化。
  3. 脏节点标记: 框架将受影响的 UI 组件标记为“脏(Dirty)”。
  4. 异步刷新: 在下一个渲染帧(Vsync)到来时,框架统一遍历脏节点列表。
  5. 属性更新: 通过后端接口直接修改 C++ 渲染树(RenderTree)的属性,屏幕显示更新。

总结:如何写出高性能代码?

  • 减少嵌套: 即使有 Diff,过深的 UI 树也会增加遍历脏节点的开销。
  • 使用 LazyForEach 处理超过 50 条的数据列表时,这是必须项,它能通过 ID Diff 极大地减少组件重建。
  • 拆分自定义组件: 将复杂的页面拆分为小的 @Component。因为状态更新的刷新范围通常限制在组件内部,拆分越细,刷新范围越小。