【HarmonyOS】状态管理V2
@ObservedV2和@Trace
使用目的
为了增强状态管理框架对类对象中属性的观测能力
使用之后嵌套对象内属性变化可直接引起UI刷新
注意事项
- 两个装饰器需要配合使用,单一使用无效
- 被Trace装饰的属性变化时,仅会通知属性关联的组件进行刷新
- 使用这两种装饰器的类,需要通过new实例化之后,才具备被观测的能力
- 不可与@State等V1装饰器混用
实践
class Son{
name:string = '张三'
}
class Father{
son: Son = new Son()
}
@Entry
struct Index{
@State param: Father = new Father()
build() {
Column(){
Text(this.param.son.name)
Button('change')
.onClick(()=>{
this.param.son.name = '李四'
})
}
}
}
在V1中,直接对嵌套对象的属性进行修改,@State是观测不到的,虽然在V1中使用@ObjectLink和@Observed可以进行观测,但是当嵌套层级过深时,代码会变得十分复杂,每个嵌套的属性类都需要创建一个新的组件,单独用@ObjectLink修饰
@ObservedV2
class Son{
@Trace name:string = '张三'
}
@ObservedV2
class Father{
son: Son = new Son()
}
@Entry
@ComponentV2
struct Index{
param: Father = new Father()
build() {
Column(){
Text(this.param.son.name)
Button('change')
.onClick(()=>{
this.param.son.name = '李四'
})
}
}
}
使用@ObservedV2和@Trace就解决这个问题,在开发过程中不再需要去修改UI部分,只需要在类型定义部分使用@Trace装饰需要渲染的属性即可
@ComponentV2
使用目的
为了让自定义组件能够使用状态管理V2的能力
注意事项
- 在该装饰器装饰的自定义组件中,能且仅能使用状态管理V2
- 暂不支持LocalStorage等原本@Component支持的能力
类比V1中的@Component,就是能让自定义组件也能使用状态管理V2的能力
@Local
使用目的
实现@ComponentV2装饰的自定义组件中变量变化的检测
注意事项
- 被装饰的变量无法从外部初始化,必须在组件内部初始化
- @Local装饰简单类型是,能观测到变量值的赋值;装饰对象类型时,仅能观测到对象整体的赋值;装饰数组类型时,能够观测到数组整体以及数组元素项的变化;装饰Array、Set、Map等类型内嵌类型,可以观测通过API调用带来的变化
- @Local支持null、undefined以及联合类型
设计目的
使用@State装饰的状态变量可以从外部初始化,因此无法保证@State装饰的变量初始值一定为组件内部定义的,不利于状态管理,而@Local就解决了这个问题
和@State的区别
@State不支持深度观测,而@Local则依赖于@Trace装饰器
@Param
使用目的
为了增强子组件接受外部参数的能力
@Param装饰的变量不仅可以接受组件外部的输入,还可以接受@Local的同步变化
注意事项
- @Param装饰的变量支持本地初始化,但不允许在组件内部直接修改
- 被装饰的变量支持初始化自定义时从外部传入,当传入的数据也为状态变量时,修改会同步给@Param
- 对于复杂对象,@Param获得的是数据源的引用。在组件中可以修改类对象的属性,修改会同步给数据源
- 对于复杂对象,@Param不允许修改引用本身,对于简单对象不允许修改值本身
设计目的
在状态管理V1中存在多种可接受外部传入的装饰器,这些装饰器使用各有限制,这些限制也让组件之间的传值规则变得非常复杂,不易使用。因此推出@Trace装饰器,就像是一个统一的接口,表示组件从外部传入的状态。
实践
@ObservedV2
class Son {
@Trace name: string = '张三'
}
@ObservedV2
class Father {
son: Son = new Son()
}
@Entry
@ComponentV2
struct Index {
@Local param: Father = new Father()
build() {
Column() {
Text('parent:' + this.param.son.name)
child({ param: this.param.son })
}
}
}
@ComponentV2
struct child {
@Param param: Son = new Son()
build() {
Column() {
Text('child:' + this.param.name)
Button('change')
.onClick(() => {
this.param.name = '李四'
})
}
}
}
由于@Param获得的是数据源的引用,所以在子组件中修改数据源的值是可以同步回父组件的
但是若保持代码结构不变,仅将V2的装饰器替换为V1的@State和@Link,则会产生运行时报错,程序闪退
@Link渲染更新流程
- 初始渲染:执行父组件的build函数,创建子组件的实例
- 指定父组件中的@State变量用于初始化子组件的@Link变量
- 父组件的状态变量包装类通过子组件的构造函数传递给子组件。子组件的@Link包装类拿到父组件的状态变量后,会将当前@Link包装类实例注册给父组件的@State变量
这里的注册意思就是,让父组件的@State包装类持有@Link的this,告诉父组件子组件的 “某某某” 正在引用它
- 数据源变化引起的@Link更新
- 在第一步中,子组件的@Link包装类把当前this指针注册给父组件。父组件的@State变更后,会遍历更新所有依赖它的系统组件和状态变量
- 通知@Link包装类更新后,子组件中依赖@Link的组件也会被更新,从而实现父->子的更新
- 在子组件内更改@Link的值
- @Link被修改之后,会调用父组件的@State包装类的set方法,将数据同步回父组件
- 父组件内@State的值修改后又会像第二步一样,遍历刷新所有的依赖的组件,实现子->父的更新
在上述的例子中,若是给@Link子组件传入一个@State的属性,就相当于是给@Link传入了一个非状态变量,@lLink无法进行注册,也就产生一些无法预知的错误
@Once
使用目的
为了实现仅从外部初始化一次且不接受后续同步变化的能力
注意事项
- @Once必须搭配@Param使用
- @Once不会影响@Param的观测能力,仅对数据源的变化做出拦截
- @Once与@Param搭配使用时,可以在本地修改@Param的值,且能够触发UI刷新
- @Once本身不具有观察能力
实践
@ObservedV2
class Son {
@Trace name: string = '张三'
}
@ObservedV2
class Father {
son: Son = new Son()
}
@Entry
@ComponentV2
struct Index {
@Local param: Father = new Father()
build() {
Column() {
Text('parent:' + this.param.son.name)
Button('change')
.onClick(() => {
this.param.son.name = '李四'
})
child({ param: this.param })
}
.size({width:'100%',height:'100%'})
}
}
@ComponentV2
struct child {
@Param @Once param: Father = new Father()
build() {
Column() {
Text('child:' + this.param.son.name)
Button('change')
.onClick(() => {
this.param = new Father()
})
}
}
}
使用@Once可以在子组件内修改@Param的值,有点类似于?@Local,不过不同的是可以接受外部赋值
@Event
使用目的
为了实现子组件与父组件之间的双向通信能力
即子组件想要修改时,不会在子组件内进行修改,而是向父组件发出修改请求,修改后再同步回自身,实现双向同步
注意事项
- @Event装饰的回调方法中的参数以及返回值由开发者决定
- @Event装饰非回调类型的变量时不会生效。当@Event没有初始化时,会自动生成一个空函数作为默认回调
- 调用完@Event的方法后子组件内的值不会立即变化,因为子组件的变化能力是交由父组件处理的,具体要看父组件内的处理细节,而且父组件将变化同步回子组件的过程也是异步的
有点类似于@Once,前者是作为一个辅助装饰器单拎出来,@Event则是像另一一个辅助装饰器与@Param的封装体,也可以接受外部赋值
实践
@Entry
@ComponentV2
struct Index {
@Local param: string = '张三'
build() {
Column() {
child({
param: this.param,
onFun: (param: number) => {
if(param === 1){
this.param = '李四'
}else{
this.param = '王五'
}
}
})
}
.size({width:'100%',height:'100%'})
}
}
@ComponentV2
struct child {
@Param param:string = ''
@Event onFun: (param: number)=>void = (param: number) =>{}
build() {
Column() {
Text('child: '+this.param)
Button('change1')
.onClick(() => {
this.onFun(1)
})
Button('change2')
.onClick(() => {
this.onFun(2)
})
}
}
}
V2中简单化了实现父子间双向同步的逻辑,不直接操作子组件内的状态变量,而是通过一个箭头函数来修改父组件中的状态变量,让开发者可以自定义子->父更新的数据相应逻辑,自由度更高
@Provider和@Consumer
与V1中的@Provide对比
- V2中的Provider支持function类型,V1不支持
- V2中的Provider使用时后面括号内的值(aliasName)是唯一匹配的key,而V1中的aliasName和属性名都为key,会优先匹配alias
- V2中的Provider不支持从父组件初始化,V1支持
- V2中的@Provider支持重名,V1不支持,若想在V1中使用(重载),则需额外配置
注意事项
- 若不给Provider配置aliasName,则会使用属性名作为默认aliasName
- @Consumer会向上查找其最近的父@Provider的数据
- 若@Consumer未找到相应的@Provider,则会变为类似于@Local装饰器单独使用
实践
@Entry
@ComponentV2
struct Index {
@Provider('key') param: string = '0'
build() {
Column() {
child1()
}
.size({width:'100%',height:'100%'})
}
}
@ComponentV2
struct child1 {
@Provider('key') param:string = '1'
build() {
Column() {
leaf()
}
}
}
@ComponentV2
struct leaf{
@Consumer('key') param: string = ''
build() {
Text(this.param)
}
}
若@Provider的别名相同,则向上查找最近的@Provider,若不存在,需要注意的是@Consumer只会向上查找父组件,不会查找兄弟组件
@Monitor
使用目的
为了增强状态管理框架对状态变量变化的监听能力
@Monitor装饰器用于监听状态变量的修改,使得状态变量具有深度监听的能力
注意事项
- 该装饰器支持在类中与@ObserbedV2、@Trace配合使用,实现对类内属性的变化监听
- 判断是否属性变化时,@Monitor使用的是严格相等(===)
- 单个@Monitor能够同时监听多个属性的变化,当这些属性在一此事件中共同变化时,只会触发一次变化回调
- 当@Monitor监听整个数组时,数组单个项的更改、内置属性的api调用,都无法被监听
- 在继承类场景中,若在父子组件中对同一属性分别设置@Monitor监听,当属性变化时,两个Monitor回调均会被调用
- 当@Monitor传入多个路径参数时,会将参数全部拼成一个字符串,并加上空格,一次来判断是否重复监听
- 若向@Monitor的路径传入一个变量,则以初始变量的值为准,后续改变变量的值不会改变Monitor的监听对象
- 当@Monitor监听的变量由可访问状态变为不可访问状态时,不会触发监听
let a = new A()//可访问
a = null//不可访问
a = undefined//不可访问
设计目的
状态管理V1无法实现对对象、数组中某个属性或者数组项变化的监听,也无法获取变化前的值,因此推出@Monitor装饰器
@Monitor与@Watch的区别
| @Watch | @Monitor | |
|---|---|---|
| 参数 | 回调方法名 | 监听目标的名称 |
| 监听能力 | T 仅能监听到引用是否改变 | 可以监听到深层变化 |
| 监听目标数量 | 一个 | 多个 |
| 能否获取变化前的值 | 不能 | 能 |
实践
@Entry
@ComponentV2
struct Index {
@Local before: string = 'default'
@Local after: string = 'default'
@Provider('key') param: Father = new Father()
@Monitor('param.son.name')
paramChange(monitor: IMonitor){
this.before = monitor.value()?.before as string
this.after = monitor.value()?.now as string
}
build() {
Column() {
Text(`before:${this.before} after: ${this.after}`)
Button('change')
.onClick(()=>{
this.param.son.name = '李四'
})
}
.size({width:'100%',height:'100%'})
}
}
@Monitor不仅可以监听到复杂对象的变化,而且可以获得变量变化前后的值,不过@Monitor的入参写法比较特殊,需要注意
@Computed
使用目的
当给UI绑定了相同的计算逻辑时,为了节约多次重复计算导致的性能问题,可以使用@Computed来计算属性
注意事项
- 不能在被装饰的getter方法中修改参与计算的属性,否则会一致循环计算导致应用冻结
- 被装饰的方法仅在初始化,或者计算的状态变量变化时,才会触发计算
- 被装饰的属性为只读,无法与双向绑定配合使用
- 计算属性本身会带来性能开销,对于简单的计算逻辑,可以不使用计算属性
- 计算属性可以初始化@Param
实践
@Entry
@ComponentV2
struct Index {
@Local param1: string = 'param1'
@Local param2: string = 'param2'
build() {
Column() {
Text(this.param1 + this.param2)
Text(this.param1 + this.param2)
}
.size({width:'100%',height:'100%'})
}
}
在上述场景中,系统组件的入参绑定了一个计算表达式,当需要用到多次相同组件时,每次都会重复计算,造成资源浪费,而使用@Computed即可解决这个问题
@Entry
@ComponentV2
struct Index {
@Local param1: string = 'param1'
@Local param2: string = 'param2'
@Computed
get getParam(){
return this.param1 + this.param2
}
build() {
Column() {
Text(this.getParam)
Text(this.getParam)
}
.size({width:'100%',height:'100%'})
}
}
@Computed结合get关键字即可避免重复计算,初始化时只会计算一次,并且将计算结果保存至缓存中,后需getter方法相关的状态变量变化时才会重新计算
@Type
使用目的
在持久化一个class时,为了实现序列化类时不丢失属性的复杂类型,可以使用@Type来装饰类属性
使用限制
- 不支持collection.Set、collection.Map等类型
- 不支持简单类型
- 不支持构造函数含参数的类
@ReusableV2
使用目的
为了降低反复创建销毁自定义组件时带来的性能开销,开发者可以使用@ReusableV2来装饰自定义组件
注意事项
- @ReusableV2同样提供了aboutToRecycle和abouotToReuse回调,但是与@Reusable不同的是,V2中的abouotToReuse(复用回调)没有入参
- 在回收阶段,框架会递归地调用所有子组件的Recycle回调;在复用阶段,则会递归地调用所有子组件的Reuse回调(即使组件未被@ReusableV2修饰)
和V1的区别
- 在V1中,组件进入复用池后仍会响应更新,带来不必要的性能消耗,需要手动使用组件冻结能力才能解决;V2在组件进入复用池时会自动冻结组件,冻结状态将持久到Reuse回调,在此之前(不包括Reuse回调)的状态变量修改都不会触发UI变化
- V2在复用之前会重置组件中的状态变量以及相关的@Computed@Monitor的内容。在复用过程中,所有V2的自定义组件都会重置,无论是否被标记
实践
@Entry
@ComponentV2
struct Index {
@Local data: MyDataSource = new MyDataSource()
aboutToAppear(): void {
for (let i = 0; i <= 200; i++) {
this.data.pushData(new NumberData(i));
}
}
build() {
List() {
LazyForEach(this.data,(item: NumberData) => {
ListItem(){
child({item: item})
}
})
}
.size({width:'100%',height:'100%'})
.cachedCount(5)
}
}
@ReusableV2
@ComponentV2
struct child{
@Param item: NumberData = new NumberData(-1)
aboutToAppear(): void {
console.log(`组件${this.item.index}已经创建`)
}
aboutToReuse(): void {
console.log(`组件${this.item.index}已经复用`)
}
aboutToRecycle(): void {
console.log(`组件${this.item.index}已经回收`)
}
build() {
Column(){
Text(`组件${this.item.index}`)
.fontSize(24)
}
.width('100%')
.height(100)
}
}
上述代码中,组件19的创建就是复用了缓存池中的组件,此时组件0被回收
更多推荐


所有评论(0)