鸿蒙 API 12 推出了 V2 版本的状态管理装饰器,比起 V1 装饰器,V2 装饰器的职责更加地清晰,并且能力也得到了加强,建议后续开发使用 V2 版本的装饰器
相关文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/_u6001_u7ba1_u7406_uff08v2_u8bd5_u7528_u7248_uff09-V5
前提 V2 相关装饰器所涉及到的根视图组件都需要用@ComponentV2 进行修饰
@ObservedV2 举例:
问题 在 V1 中,@Observed 无法深度观测,比如以下的例子
先点击修改名字,名字变化,再点击修改班级,班级不会变化
先点击修改班级,班级不会变化,再点击修改名字,名字和班级同时发生变化
结论:
直接修改第二层的属性,虽然对象本身已经发生了变化,但是无法被@Observed 观测到 修改第一层的属性的时候,会被@Observed 观测到,执行 UI 刷新,并且刷新是全体的,并不只是修改绑定的属性对应的 UI (这里会引申出一个性能问题:如果某个对象有很多属性,只改变一个属性会导致其他无关控件也被刷新,造成无意义的性能损耗)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Observed class Grade { grade: number name: string constructor (grade: number , name: string ) { this .grade = grade this .name = name } } @Observed class Student { name: string age: number grade: Grade constructor (name: string , age: number , grade: Grade ) { this .name = name this .age = age this .grade = grade } } @Entry @Component struct Index { @State student: Student = new Student("John" ,18 , new Grade(3 ,"B" )) build ( ) { Column ({ space: 20 } ) { Blank() Text("Name: " + this .student.name) .fontWeight(FontWeight.Bold) Text("Age: " + this .student.age) .fontWeight(FontWeight.Bold) Row ({ space: 20 } ) { Text("Grade: " + this .student.grade.grade) .fontWeight(FontWeight.Bold) Text("Class: " + this .student.grade.name) .fontWeight(FontWeight.Bold) } Button("修改名字" ) .onClick(() => { this .student.name = "Mike" }) Button("修改班级" ) .onClick(() => { this .student.grade.name = "C" }) Blank() } .width("100%" ) .height('100%' ) } }
解决 被观察的类使用@ObservedV2 修饰,需要被观察到属性用@Trace 修饰即可
结果:
点击修改班级的时候,班级会发生变化;然而,点击修改名字的时候,名字不会变化
结论:
只有被@Trace 修饰的属性,修改的时候才会发生变化,没有@Trace 修饰的属性是不会被观察到的 使用该修饰器修饰的属性,被修改的时候只会重新渲染相关的组件,其他没有被修改的属性是不会重新渲染的,比起 V1 性能更好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @ObservedV2 class Grade { grade: number @Trace name: string constructor (grade: number , name: string ) { this .grade = grade this .name = name } } @ObservedV2 class Student { name: string age: number grade: Grade constructor (name: string , age: number , grade: Grade ) { this .name = name this .age = age this .grade = grade } }
其他 @Trace 支持 model 嵌套,支持继承,支持修饰数组/Map/Date/Set
@Local 问题
@Local 与@State 用法相同,区别在于@State 的属性可以外部赋值,而@Local 不允许
如果 student 用@State 修饰,下面代码是允许的, 如果 student 用@Local 修饰,则下面代码不允许
1 Index({ student : new Student("John" ,18, new Grade(3,"B" ) ) })
上述的@ObserveV2 和@Trace 只能用于监听对象的属性,但是如果对象本身发生了变化,那 UI 不会刷新,如下代码点击“修改整体”, UI 不会刷新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Entry @ComponentV2 struct Index { student: Student = new Student("John" ,18, new Grade(3,"B" ) ) build() { Column({ space : 20 }) { Blank() Text("Name: " + this .student .name ) .fontWeight(FontWeight.Bold) Text("Age: " + this .student .age ) .fontWeight(FontWeight.Bold) Row({ space : 20}) { Text("Grade: " + this .student .grade .grade ) .fontWeight(FontWeight.Bold) Text("Class: " + this .student .grade .name ) .fontWeight(FontWeight.Bold) } Button("修改整体" ) .onClick(() => { this.student = new Student("Date" , 23, new Grade(23, "None" ) ) }) Blank() } .width("100%" ) .height('100 %') } }
解决 使用@Local
1 @Local student: Student = new Student ("John" ,18 , new Grade (3 ,"B" ))
@Param 搭配@Local 使用,用于父子组件数据单向传递,复杂类型比起@Prop 性能更好
@Require 表示必须外部传入,本地不用初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Entry @ComponentV2 struct Index { @Local student: Student = new Student("John" ,18, new Grade(3,"B" ) ) build() { Column({ space : 20 }) { Blank() Info({ student : this .student }) Button("修改整体" ) .onClick(() => { this.student = new Student("Date" , 23, new Grade(23, "None" ) ) }) Blank() } .width("100%" ) .height('100 %') } } @ComponentV2 struct Info { @Require @Param student: Student build() { Column({ space : 20 }) { Text("Name: " + this .student .name ) .fontWeight(FontWeight.Bold) Text("Age: " + this .student .age ) .fontWeight(FontWeight.Bold) Row({ space : 20}) { Text("Grade: " + this .student .grade .grade ) .fontWeight(FontWeight.Bold) Text("Class: " + this .student .grade .name ) .fontWeight(FontWeight.Bold) } } } }
@Param 修饰的属性如果是对象的话,可以在子组件内部修改对象的成员属性,此时会同步到父组件 但是不可以修改对象本身
@Once 修饰@Param 修饰的属性,表示属性只能被初始化一次,后面不再改变,就算子组件或者父组件修改了这个属性,值也不会改变
1 2 3 4 5 6 @ComponentV2 struct Info { @Require @Param student : Student; @Once @Param type : number; ... }
@Event 因为@Param 修饰的变量不可以在子组件修改,所以推出@Event 补充这个功能 使用方法是修饰一个方法类型,回调属性回去给父组件进行修改 @Event 只能修饰方法类型 @Event 更多只是作为一种规范,其实不加这个修饰符也行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Entry @ComponentV2 struct Index { @Local student: Student = new Student("John" ,18, new Grade(3,"B" ) ) build() { Column({ space : 20 }) { Blank() Info({ student : this .student , change : (student : Student) => { this.student = student }}) Blank() } .width("100%" ) .height('100 %') } } @ComponentV2 struct Info { @Require @Param student: Student @Event change: (x : Student ) => void = () => {} build() { Column({ space : 20 }) { Text("Name: " + this .student .name ) .fontWeight(FontWeight.Bold) Text("Age: " + this .student .age ) .fontWeight(FontWeight.Bold) .onClick(() => { this.change( new Student("Info" , 33, new Grade(33, "Info What" ) ) ) }) Row({ space : 20}) { Text("Grade: " + this .student .grade .grade ) .fontWeight(FontWeight.Bold) Text("Class: " + this .student .grade .name ) .fontWeight(FontWeight.Bold) }.onClick(() => { this.student.name = "Info Name" }) } } }
!! 配合@Event 和@Param 进行父子组件双向绑定 子组件需要定义一个@Param 属性和一个同名@Event 并加上$,在点击按钮的时候调用@Event 的属性回调新值出去
1 2 3 4 5 6 7 8 9 10 11 12 @ComponentV2 struct Info { @Require @Param student: Student; @Event $student: (x: Student) => void = (x: Student) => {} build() { Button("Change Student" ) .onClick(() => { // 无效代码:this .student = new Student("Info" , 33 , new Grade(33 , "Info What" )) this .$student(new Student("Info" , 33 , new Grade(33 , "Info What" ))) }) } }
父组件传参数给子组件的时候加上!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Entry @ComponentV2 struct Index { @Local student : Student = new Student("John" ,18 , new Grade(3 ,"B" )) build() { Column ({ space: 20 }) { Blank () Info ({ student: this.student!! }) Blank () } .width ("100%" ) .height ('100%' ) } }
实现效果为子组件修改传入的属性本身的时候,父子组件的属性值同时发生变化
@Computed @Compute 可以把某个属性标记为计算属性,如下代码的 sum,注意此时 sum 不是方法,sum 依旧作为属性存在 当点击“修改 Grade”的时候,页面发生变化,实时显示 sum 计算后的结果,并且多次点击,不会重复执行 get 方法体 注意:sum 会发生变化的前提是,参与 sum 计算的属性需要被@Trace 修饰,如果参与 sum 计算的属性没有被监听的能力的话,那就不会触发计算方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Entry @ComponentV2 struct Index { @Local student: Student = new Student("John" ,18 , new Grade(3 ,"B" )) @Computed get sum () { let result = this .student.age + this .student.grade.grade console .log(`result = ${result} ` ) return result } build ( ) { Column ({ space: 20 } ) { Blank() Text(`sum ${this .sum} ` ) .fontWeight(FontWeight.Medium) Button("修改Grade" ) .onClick(() => { this .student.grade.grade = 30 }) Blank() } .width("100%" ) .height('100%' ) } }
@Provider 和@Consumer 使用
用于父组件和子孙组件跨层级数据沟通,只要在同个层级树上,都可以获取到对应的数据
如下代码,点击 Change 按钮的时候,父组件和孙组件的 student2.name 都会更新 UI(前提是 name 属性加上了@Trace)
也可以直接更新 student2 本身 3. 因为是跨组件传递,所以即使 Info 组件不声明 student2 属性,OtherInfo 组件也可以通过@Comsumer 得到祖父组件的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Entry @ComponentV2 struct Index { @Local student: Student = new Student("John" ,18, new Grade(3,"B" ) ) @Provider() student2: Student = new Student("Mary" ,20, new Grade(4,"B" ) ) build() { Column({ space : 20 }) { Blank() Text(`name : ${this .student2 .name }`) Text(`age : ${this .student2 .age }`) Info() Blank() } .width("100%" ) .height('100 %') } } @ComponentV2 struct Info { @Consumer() student2: Student = new Student("" ,0,new Grade(0,"" ) ) build() { Column({ space : 20 }) { OtherInfo() Button("Change" ) .onClick(() => { this.student2.name = "Sarah" }) } } } @ComponentV2 struct OtherInfo { @Consumer() student2: Student = new Student("" ,0,new Grade(0,"" ) ) build() { Row({space : 20}) { Text(`name : ${this .student2 .name }`) Text(`age : ${this .student2 .age }`) } .backgroundColor(Color.Red) } }
别名 @Provider 加上别名改为 stu, 那么子组件也要改成 stu 才能访问得到,如果还是使用 student2,则反问不到 同理@Consumer 也可以加上别名,那么孙组件的@Comsumer 就得对应改成相应的别名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Entry @ComponentV2 struct Index { @Provider('stu ') student2: Student = new Student("Mary" ,20, new Grade(4,"B" ) ) build() { Column({ space : 20 }) { Blank() Text(`name : ${this .student2 .name }`) Text(`age : ${this .student2 .age }`) Info() Blank() } .width("100%" ) .height('100 %') } } @ComponentV2 struct Info { @Consumer() stu: Student = new Student("" ,0,new Grade(0,"" ) ) build() { Column({ space : 20 }) { OtherInfo() Button("Change" ) .onClick(() => { this.student2 = new Student("333" ,0,new Grade(0,"222" ) ) }) } } }
@Monitor 介绍 可以在类或者结构体中监听某个属性或者某个对象的属性,只能修饰方法,并且方法固定一个参数为 IMoitor 相比起 V1 的@Watch 功能更加强大,可以获取到更新前后的值 IMonitor 类型: IMonitor 类型的变量用作@Monitor 装饰方法的参数。
属性
类型
参数
返回值
说明
dirty
Array
无
无
保存发生变化的属性名。
value
function
path?: string
IMonitorValue
获得指定属性(path)的变化信息。当不填 path 时返回@Monitor 监听顺序中第一个改变的属性的变化信息。
IMonitorValue类型: IMonitorValue类型保存了属性变化的信息,包括属性名、变化前值、当前值。
属性
类型
说明
before
T
监听属性变化之前的值。
now
T
监听属性变化之后的当前值。
path
string
监听的属性名。
使用
使用@Monitor 加上要监听的属性名,如果是监听对象的属性,就使用点语法(需要属性被@Trace 修饰)
监听数组的时候可以用.0、.1 的方式表示要监听的内容下标,比如@Monitor(“dimensionTwo.0.0”, “dimensionTwo.1.1”)
监听多个属性用逗号分隔
可以用多个@Monitor,但是如果监听同个属性,那么只有最后一个方法会生效
如果监听的对象变了,但是对象里面的所有属性都与变化前的一致,那么不会触发监听回调
监听时标记的要监听的属性字符串需要初始化时确定,初始化后无法修改监听的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Entry @ComponentV2 struct Index { @Local student: Student = new Student("John" ,18 , new Grade(3 ,"B" )) @Local count: number = 20 @Monitor ("student.name" , "count" ) onChange (monitor: IMonitor ) { monitor.dirty.forEach((path: string ) => { console .log(`${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now} ` ) }) } build ( ) { Column ({ space: 20 } ) { Blank() Text("Name: " + this .student.name) .fontWeight(FontWeight.Bold) Button("修改count" ) .onClick(() => { this .count += 1 this .student.age = this .count this .student.name = `name_${this .count} ` }) Blank() } .width("100%" ) .height('100%' ) } }
@Type 当持久化数据的时候,遇到嵌套属性,需要标记为@Type,目的是反序列化的时候能够保持原来的类型信息 这样子使用 PersistenceV2 获取数据的时候能够维持原数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { Type } from '@kit.ArkUI' ;@ObservedV2 class SampleChild { @Trace p1: number = 0 ; p2: number = 10 ; } @ObservedV2 export class Sample { @Type (SampleChild) @Trace f: SampleChild = new SampleChild(); } import { PersistenceV2 } from '@kit.ArkUI' ;import { Sample } from '../Sample' ;@Entry @ComponentV2 struct Page { prop: Sample = PersistenceV2.connect(Sample, () => new Sample())!; build ( ) { Column ( ) { Text(`Page1 add 1 to prop.p1: ${this .prop.f.p1} ` ) .fontSize(30 ) .onClick(() => { this .prop.f.p1++; }) } } }