ArkUI -- 状态管理的更新机制
状态管理 V1 和 V2 触发 UI 更新
状态管理 V1 使用代理观察数据,创建状态变量时,会同时创建一个代理观察者,该观察者可以感知代理变化,但无法精准观测到实际数据变化。
状态管理 V2 增强了数据的观察能力,使数据本身可观察,更新数据时,会触发相应视图的更新。
| 特性 | 状态管理 V1 | 状态管理 V2 |
|---|---|---|
| 独立于 UI | 状态变量不能独立于 UI | 状态变量独立于 UI,更新数据会触发相应视图的更新 |
| 深度观测 | 无法深度观测和深度监听,只能感知对象属性第一层的变化 | 支持对象的深度观测和深度监听,且不影响观测性能 |
| 精准更新 | 更新对象中属性时,存在冗余更新的问题 | 支持对象中属性级精准更新 |
| 易用性 | 装饰器间配合使用限制多,不易用,不利于组件化 | 装饰器易用性高、拓展性强,有利于组件化 |
状态管理的更新机制
状态管理原理介绍
状态管理的核心逻辑是处理状态变量、自定义组件和系统组件之间的绑定关系。
状态管理循环执行两大步骤:收集依赖和触发更新
- 收集与状态变量绑定的组件标识
- 当状态变量发生变化时,对收集的组件执行标 “脏”,刷新对应的 UI,同时更新依赖观察
状态管理的基本流程如图:
收集依赖
收集依赖是指建立状态变量与组件之间的数据绑定关系。
一个 UI 界面可能使用了多个状态变量,在修改状态变量时,仅与其相关的组件进行 UI 刷新,其他不相关的组件不会刷新。因此,UI 的刷新需要明确哪些组件使用了被修改的状态变量,以实现这些组件的精准刷新。
每个状态变量中都维护了一个 Set 集合,保存所有与其绑定的组件的标识信息 (系统组件的唯一标识 elementId),即框架收集依赖的过程
【示例】
@Entry@Componentstruct Index{@Stateage:number=10;@Stategrade:number=5;build(){Column(){Text('age is: '+this.age)// Text1Text('grade is: '+this.grade)// Text1Button('change age')// Button1.onClick(()=>{this.age++;})Button('change grade')// Button2.onClick(()=>{this.grade++;})}}}上述示例,Index 中定义了两个状态变量 age 和 grade,框架收集依赖的步骤为:
- 调用 build 方法,创建自定义组件 Index
- 执行到
Text('age is: ' + this.age)时,为了显示文本内容,需要读取 this.age 的值 (Text2 同理) - age 和 grade 都是 @State 装饰的状态变量,其在被读取时会收集当前正在渲染的系统组件的唯一标识 elementId,并将其存储到状态变量维护的 Set 集合中
触发更新
当状态变量发生改变时,状态管理框架会通知所有依赖于它的 UI 组件,重新计算并刷新,此过程称为触发更新。
触发更新可分为三个步骤:
- 计算状态变量发生改变后的新值
- 修改状态变量的值,并将与其绑定的组件标脏
- 刷新所有标脏的节点,更新 UI 的同时重新收集依赖
更新是以自定义组件为单位的,每个自定义组件中维护了一个标脏的系统组件集合,用于保存在当前 UI 更新周期中标脏的系统组件的 elementId,即需要刷新的组件标识
触发更新就是,当状态变量发生改变时,将其收集到的依赖组件,标记为“脏”,在下一个 UI 更新周期中,只刷新标脏的组件,实现最小化更新。
同样对上述示例代码,点击 Button 组件修改状态变量,对应的 Text 组件刷新,具体步骤为:
- 在点击事件中处理
this.age++,由于 age 是状态变量,在改值的过程中会执行状态管理内部的更新操作 - 在状态变量 age 的更新操作中,将其依赖集合中系统组件的 elementId 加入到其所属自定义组件 Index 的脏系统组件集合中 (每个自定义组件中维护了一个标脏的系统组件集合,用于保存在当前 UI 更新周期中标脏的系统组件的 elementId)
- 完成系统组件标脏后,将状态变量 age 所属的自定义组件 Index 标脏,加入到标脏的自定义组件节点列表中 (脏自定义组件列表),并请求一个刷新信号 (Vsync 信号)
- 在下一个 UI 更新周期中 (下一个帧信号),框架遍历脏自定义组件列表,重新调用其
rerender方法 (rerender 方法是由系统生成的),遍历自定义组件 Index 中的脏系统组件,刷新 Text 组件并更新依赖
状态管理 V1 和 V2 的更新机制差异
相比于 V1 状态管理,状态管理 V2 在状态变量变化时,会异步标脏组件,如下图:
V1组件的更新
- 步骤1:事件触发修改 V1 状态变量,观察状态变量的变化
- 步骤2:执行 @Watch 回调
- 步骤3:执行节点标脏,将脏节点放在脏节点列表中,并请求
Vsync信号 - 步骤4:更新脏节点列表,先更新父组件,再更新子组件
- 步骤5:若状态变量再次发生变化,会执行步骤4,步骤4在一个 Vsync 周期内的迭代不会超过 3 次,第 3 次迭代后,标脏的节点只会加到脏节点列表,等下一个 Vsync 进行脏节点更新
V2组件的更新
V2 状态管理相比 V1,新增异步执行 @Computed,@Monitor 和 节点标脏
- 步骤1:事件触发修改 V2 状态变量,抛
Promise异步任务 - 步骤2:等待事件的逻辑处理完成后 (如 onClick 事件),执行步骤1抛出的 Promise 回调
- 步骤3:执行 @Computed 变量更新
- 步骤4:若 @Computed 回调中有状态变量的变化,回到步骤3,否则进入步骤5
- 步骤5:执行 @Monitor 回调函数
- 步骤6:若 @Monitor 回调中有状态变量的变化,则进入步骤3,否则进入步骤7
- 步骤7:执行节点标脏,将脏节点放在脏节点列表中,并请求
Vsync信号 - 步骤8:更新脏节点列表,先更新父组件,再更新子组件 (同 V1 组件的更新,一个 Vsync 周期内迭代不会超过 3 次)
- V1 状态变量的变化,会触发 @Watch 的同步执行,若状态变量被修改多次,则 @Watch 回调会同步执行多次
- V2 状态变量的变化,会触发 @Monitor 的异步执行,若在一次事件中状态变量被多次修改,@Monitor 回调只会执行一次
