跳到主要内容

状态管理原理介绍

本文将介绍状态管理的基本原理。状态管理的核心逻辑是处理状态变量、自定义组件和系统组件之间的绑定关系。其工作流程可以概括为两个核心阶段:收集依赖和触发更新。

收集依赖

收集依赖是指建立状态变量与组件之间的数据绑定关系。在UI渲染时,状态管理框架会“观察”哪些状态变量被读取了,并记录下这个“依赖关系”。一个UI界面上可能使用了多个状态变量,在修改状态变量时,仅与该状态变量相关的组件进行UI刷新,其他不相关的组件不会刷新。因此,UI刷新时需要明确哪些组件使用了被修改的状态变量,以能够实现这些组件的精准刷新。

@Entry
@Component
struct Index {
@State name: string = 'Jack';
@State age: number = 10;
@State grade: number = 5;

build() {
Column() {
Text(`${this.name}'s age is ${this.age}`) // Text1
Text(`${this.name}'s grade is ${this.grade}`) // Text2
Button('change age') // Button1
.onClick(() => {
this.age++;
})
Button('change grade') // Button2
.onClick(() => {
this.grade++;
})
}
}
}

上述示例代码中,自定义组件Index中定义了三个状态变量name,age和grade,在build函数中创建了两个Text系统组件和两个Button系统组件。收集依赖的具体步骤为:

  1. 自定义组件Index被创建并首次调用build方法创建组件。
  2. 当框架执行到Text(${this.name}'s age is ${this.age})时,为了显示文本内容,需要读取this.name和this.age的值。
  3. name和age都是被@State装饰器装饰的状态变量,状态变量在被读取时会收集当前正在渲染的系统组件的唯一标识elementId,并将其存储到一个Set集合中。因此,状态变量name和age均收集到Text1的唯一标识elementId1。
  4. 同理,当框架执行到Text(${this.name}'s grade is ${this.grade})时,状态变量name和grade收集到了Text2的唯一标识elementId2。

每个状态变量中维护了一个Set集合,保存所有与其绑定的系统组件的标识信息。在上述示例中,状态变量name的依赖集合中保存了Text1和Text2的信息,状态变量age的依赖集合中保存了Text1的信息,状态变量grade中保存了Text2的信息。由此,框架完成了收集依赖的过程。

触发更新

当状态变量发生改变时,状态管理框架会通知所有依赖于它的UI组件,重新计算并刷新,这个过程称为触发更新。触发更新大致可以分为三个步骤:

  • 计算状态变量发生改变后的新值。
  • 修改状态变量的值,并将与其绑定的组件标脏。
  • 刷新所有的脏节点,更新UI的同时重新收集依赖。

更新是以自定义组件为单位的。

同样对于上述示例代码,点击Button组件修改状态变量,对应的Text组件刷新,具体步骤为:

  1. 点击Button1,触发 onClick 事件。
  2. 在事件处理函数中执行this.age++。由于age是状态变量,在改值的过程中会执行状态管理内部的更新操作。
  3. 由于在一个UI更新周期中,自定义组件中可能存在多个状态变量发生改变,而更新是以自定义组件为单位的,所以每个自定义组件中维护了一个标脏的系统组件集合(下称脏系统组件集合),用于保存在当前UI更新周期中标脏的系统组件的elementId。在状态变量age的更新操作中,将其依赖集合中系统组件的elementId加入到其所属的自定义组件Index的脏系统组件集合中。
  4. 完成系统组件标脏后,将状态变量age所属的自定义组件Index标脏,加入到标脏的自定义组件节点列表(下称脏自定义组件列表)中,并请求一个刷新信号。
  5. 在下一个UI更新周期中,框架遍历脏自定义组件列表,重新调用它们的rerender方法(rerender方法是由系统生成的),执行Index自定义组件的rerender方法时,遍历脏系统组件,刷新Text1组件并更新依赖。
  6. 同理,点击Button2修改状态变量grade的值,对应刷新Text2组件并更新依赖。

触发更新就是根据状态变量收集到的依赖关系,当状态变量发生改变时,找到所有受影响的组件,标记为“脏”,在一个UI更新周期中,只刷新标脏的组件,实现最小化更新。

状态管理在渲染管线中的流程

UI渲染的流程主要有以下几个步骤:

  1. 事件触发状态变量发生改变,执行状态变量的set方法,将自定义组件和系统组件标脏,并请求一个刷新信号。
  2. 刷新脏节点:刷新标脏的自定义组件和系统组件。
  3. 布局:根据标脏局部刷新组件树,触发子树上节点的尺寸测量和位置确认。

同样以上述示例代码为例,使用DevEco Studio的Profiler工具,点击Button1,抓取状态变量的变化打点,trace如下图所示:

对上图中的标记点进行逐一介绍:

  1. 点击Button1按钮,产生手势事件的trace点。
  2. 手势事件触发onClick回调。
  3. 在onClick回调中改变状态变量age的值。
  4. 下一帧信号到来,执行VSync回调。
  5. 刷新脏自定义组件Index。
  6. 遍历自定义组件中的脏系统组件,重新渲染Text1组件。
  7. 执行后续布局流程。

其中,状态管理的基本流程如图所示:

状态管理循环执行两大步骤:收集依赖和触发更新。收集状态变量与组件之间的依赖关系。当状态变量发生变化时,执行标脏,刷新对应的UI,同时更新依赖关系。

相比状态管理V1,状态管理V2在状态变量变化时,会异步标脏组件,这两者的更新差异详细可参考状态管理V1和V2更新机制差异文档。