状态管理 V1 使用代理观察数据,创建状态变量时,会同时创建一个代理观察者,该观察者可以感知代理变化,但无法精准观测到实际数据变化。V1 侧重于组件层级的状态管理,V1 若要实现深度观察能力,需通过 @ObjectLink 逐层拆解嵌套类,并配合自定义组件使用。

状态管理 V2 则增强了对数据对象的深度观察与管理能力,使数据本身可观察,更新数据时,会触发相应视图的更新,UI刷新更加高效,可灵活地控制数据与状态,提升应用性能。

特性 状态管理 V1 状态管理 V2
独立于 UI 状态变量不能独立于 UI 状态变量独立于 UI,更新数据会触发相应视图的更新
深度观测 无法深度观测和深度监听,只能感知对象属性第一层的变化 支持对象的深度观测和深度监听,且不影响观测性能
精准更新 更新对象中属性时,存在冗余更新的问题 支持对象中属性级精准更新
易用性 装饰器间配合使用限制多,不易用,不利于组件化 装饰器易用性高、拓展性强,有利于组件化

状态管理原理介绍

状态管理的核心逻辑是处理状态变量、自定义组件和系统组件之间的绑定关系。

状态管理循环执行两大步骤:收集依赖和触发更新

  • 收集与状态变量绑定的组件标识
  • 当状态变量发生变化时,对收集的组件执行标 “脏”,刷新对应的 UI,同时更新依赖观察

状态管理的基本流程如图:
在这里插入图片描述

收集依赖

收集依赖是指建立状态变量与组件之间的数据绑定关系。

一个 UI 界面可能使用了多个状态变量,在修改状态变量时,仅与其相关的组件进行 UI 刷新,其他不相关的组件不会刷新。因此,UI 的刷新需要明确哪些组件使用了被修改的状态变量,以实现这些组件的精准刷新。

每个状态变量中都维护了一个 Set 集合,保存所有与其绑定的组件的标识信息 (系统组件的唯一标识 elementId),即框架收集依赖的过程

【示例】

	@Entry
	@Component
	struct Index {
	  @State age: number = 10;
	  @State grade: number = 5;
	
	  build() {
	    Column() {
	      Text('age is: ' + this.age) // Text1
	      Text('grade is: ' + this.grade) // Text1
	     
	      Button('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 回调只会执行一次
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐