装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
说明
从API version 9开始,这两个装饰器支持在ArkTS卡片中使用。
从API version 11开始,这两个装饰器支持在元服务中使用。
一、概述
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
- 被@Observed装饰的类,可以被观察到属性的变化;
- 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
- @Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用(示例详见嵌套对象),如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用(示例详见@Prop与@ObjectLink的差异)。
二、限制条件
- 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
- @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。
三、装饰器说明
@Observed类装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
@ObjectLink变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
允许装饰的变量类型 | 必须为被@Observed装饰的class实例,必须指定类型。 不支持简单类型,可以使用@Prop。 支持继承Date、Array的class实例,API11及以上支持继承Map、Set的class实例。示例见观察变化。 API11及以上支持@Observed装饰类和undefined或null组成的联合类型,比如ClassA | ClassB, ClassA | undefined 或者 ClassA | null, 示例见@ObjectLink支持联合类型。@ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。 |
被装饰变量的初始值 | 不允许。 |
@ObjectLink装饰的数据为可读示例。
// 允许@ObjectLink装饰的数据属性赋值
this.objLink.a= ...
// 不允许@ObjectLink装饰的数据自身赋值
this.objLink= ...
说明
@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
- @Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;
- @ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。禁止对@ObjectLink装饰的变量赋值,如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。
四、变量的传递/访问规则说明
@ObjectLink传递/访问 | 说明 |
---|---|
从父组件初始化 | 必须指定。 初始化@ObjectLink装饰的变量必须同时满足以下场景: - 类型必须是@Observed装饰的class。 - 初始化的数值需要是数组项,或者class的属性。 - 同步源的class或者数组必须是@State,@Link,@Provide,@Consume或者@ObjectLink装饰的数据。 同步源是数组项的示例请参考对象数组。初始化的class的示例请参考嵌套对象。 |
与源对象同步 | 双向。 |
可以初始化子组件 | 允许,可用于初始化常规变量、@State、@Link、@Prop、@Provide |
初始化规则图示
五、观察变化和行为表现
5.1 观察变化
@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被@Observed装饰,否则将观察不到其属性的变化。
class Child {public num: number;constructor(num: number) {this.num = num;}
}@Observed
class Parent {public child: Child;public count: number;constructor(child: Child, count: number) {this.child = child;this.count = count;}
}
以上示例中,Parent被@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于Child,没有被@Observed装饰,其属性的修改不能被观察到。
@ObjectLink parent: Parent// 赋值变化可以被观察到
this.parent.child = new Child(5)
this.parent.count = 5// Child没有被@Observed装饰,其属性的变化观察不到
this.parent.child.num = 5
@ObjectLink:@ObjectLink只能接收被@Observed装饰class的实例,推荐设计单独的自定义组件来渲染每一个数组或对象。此时,对象数组或嵌套对象(属性是对象的对象称为嵌套对象)需要两个自定义组件,一个自定义组件呈现外部数组/对象,另一个自定义组件呈现嵌套在数组/对象内的类对象。可以观察到:
- 其属性的数值的变化,其中属性是指Object.keys(observedObject)返回的所有属性,示例请参考嵌套对象。
- 如果数据源是数组,则可以观察到数组item的替换,如果数据源是class,可观察到class的属性的变化,示例请参考对象数组。
继承Date的class时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Observed
class DateClass extends Date {constructor(args: number | string) {super(args)}
}@Observed
class NewDate {public data: DateClass;constructor(data: DateClass) {this.data = data;}
}@Component
struct Child {label: string = 'date';@ObjectLink data: DateClass;build() {Column() {Button(`child increase the day by 1`).onClick(() => {this.data.setDate(this.data.getDate() + 1);})DatePicker({start: new Date('1970-1-1'),end: new Date('2100-1-1'),selected: this.data})}}
}@Entry
@Component
struct Parent {@State newData: NewDate = new NewDate(new DateClass('2023-1-1'));build() {Column() {Child({ label: 'date', data: this.newData.data })Button(`parent update the new date`).onClick(() => {this.newData.data = new DateClass('2023-07-07');})Button(`ViewB: this.newData = new NewDate(new DateClass('2023-08-20'))`).onClick(() => {this.newData = new NewDate(new DateClass('2023-08-20'));})}}
}
继承Map的class时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。详见继承Map类。
继承Set的class时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详见继承Set类。
六、框架行为
6.1 初始渲染:
- @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
- 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。
6.2 属性更新:
当@Observed装饰的class属性改变时,会执行到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。
七、使用场景
7.1 嵌套对象
说明
NextID是用来在ForEach循环渲染过程中,为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。
// objectLinkNestedObjects.ets
let NextID: number = 1;@Observed
class Bag {public id: number;public size: number;constructor(size: number) {this.id = NextID++;this.size = size;}
}@Observed
class User {public bag: Bag;constructor(bag: Bag) {this.bag = bag;}
}@Observed
class Book {public bookName: BookName;constructor(bookName: BookName) {this.bookName = bookName;}
}@Observed
class BookName extends Bag {public nameSize: number;constructor(nameSize: number) {// 调用父类方法对nameSize进行处理super(nameSize);this.nameSize = nameSize;}
}@Component
struct ViewA {label: string = 'ViewA';@ObjectLink bag: Bag;build() {Column() {Text(`ViewA [${this.label}] this.bag.size = ${this.bag.size}`).fontColor('#ffffffff').backgroundColor('#ff3d9dba').width(320).height(50).borderRadius(25).margin(10).textAlign(TextAlign.Center)Button(`ViewA: this.bag.size add 1`).width(320).backgroundColor('#ff17a98d').margin(10).onClick(() => {this.bag.size += 1;})}}
}@Component
struct ViewC {label: string = 'ViewC1';@ObjectLink bookName: BookName;build() {Row() {Column() {Text(`ViewC [${this.label}] this.bookName.size = ${this.bookName.size}`).fontColor('#ffffffff').backgroundColor('#ff3d9dba').width(320).height(50).borderRadius(25).margin(10).textAlign(TextAlign.Center)Button(`ViewC: this.bookName.size add 1`).width(320).backgroundColor('#ff17a98d').margin(10).onClick(() => {this.bookName.size += 1;console.log('this.bookName.size:' + this.bookName.size)})}.width(320)}}
}@Entry
@Component
struct ViewB {@State user: User = new User(new Bag(0));@State child: Book = new Book(new BookName(0));build() {Column() {ViewA({ label: 'ViewA #1', bag: this.user.bag }).width(320)ViewC({ label: 'ViewC #3', bookName: this.child.bookName }).width(320)Button(`ViewB: this.child.bookName.size add 10`).width(320).backgroundColor('#ff17a98d').margin(10).onClick(() => {this.child.bookName.size += 10console.log('this.child.bookName.size:' + this.child.bookName.size)})Button(`ViewB: this.user.bag = new Bag(10)`).width(320).backgroundColor('#ff17a98d').margin(10).onClick(() => {this.user.bag = new Bag(10);})Button(`ViewB: this.user = new User(new Bag(20))`).width(320).backgroundColor('#ff17a98d').margin(10).onClick(() => {this.user = new User(new Bag(20));})}}
}
效果图
被@Observed装饰的BookName类,可以观测到继承基类的属性的变化。
ViewB中的事件句柄:
- this.user.bag = new Bag(10) 和this.user = new User(new Bag(20)): 对@State装饰的变量size和其属性的修改。
- this.child.bookName.size += … :该变化属于第二层的变化,@State无法观察到第二层的变化,但是Bag被@Observed装饰,Bag的属性size的变化可以被@ObjectLink观察到。
ViewC中的事件句柄:
- this.bookName.size += 1:对@ObjectLink变量size的修改,将触发Button组件的刷新。@ObjectLink和@Prop不同,@ObjectLink不拷贝来自父组件的数据源,而是在本地构建了指向其数据源的引用。
- @ObjectLink变量是只读的,this.bookName = new bookName(…)是不允许的,因为一旦赋值操作发生,指向数据源的引用将被重置,同步将被打断。
八、对象数组
对象数组是一种常用的数据结构。以下示例展示了数组对象的用法。
let NextID: number = 1;@Observed
class Info {public id: number;public info: number;constructor(info: number) {this.id = NextID++;this.info = info;}
}@Component
struct Child {// 子组件Child的@ObjectLink的类型是Info@ObjectLink info: Info;label: string = 'ViewChild';build() {Row() {Button(`ViewChild [${this.label}] this.info.info = ${this.info ? this.info.info : "undefined"}`).width(320).margin(10).onClick(() => {this.info.info += 1;})}}
}@Entry
@Component
struct Parent {// Parent中有@State装饰的Info[]@State arrA: Info[] = [new Info(0), new Info(0)];build() {Column() {ForEach(this.arrA,(item: Info) => {Child({ label: `#${item.id}`, info: item })},(item: Info): string => item.id.toString())// 使用@State装饰的数组的数组项初始化@ObjectLink,其中数组项是被@Observed装饰的Info的实例Child({ label: `ViewChild this.arrA[first]`, info: this.arrA[0] })Child({ label: `ViewChild this.arrA[last]`, info: this.arrA[this.arrA.length-1] })Button(`ViewParent: reset array`).width(320).margin(10).onClick(() => {this.arrA = [new Info(0), new Info(0)];})Button(`ViewParent: push`).width(320).margin(10).onClick(() => {this.arrA.push(new Info(0))})Button(`ViewParent: shift`).width(320).margin(10).onClick(() => {if (this.arrA.length > 0) {this.arrA.shift()} else {console.log("length <= 0")}})Button(`ViewParent: item property in middle`).width(320).margin(10).onClick(() => {this.arrA[Math.floor(this.arrA.length / 2)].info = 10;})Button(`ViewParent: item property in middle`).width(320).margin(10).onClick(() => {this.arrA[Math.floor(this.arrA.length / 2)] = new Info(11);})}}
}
效果图
this.arrA[Math.floor(this.arrA.length/2)] = new Info(…) :该状态变量的改变触发2次更新:
- ForEach:数组项的赋值导致ForEach的itemGenerator被修改,因此数组项被识别为有更改,ForEach的item builder将执行,创建新的Child组件实例。
- Child({ label: ViewChild this.arrA[last], info: this.arrA[this.arrA.length-1] }):上述更改改变了数组中第二个元素,所以绑定this.arrA[1]的Child将被更新。
this.arrA.push(new Info(0)) : 将触发2次不同效果的更新:
- ForEach:新添加的Info对象对于ForEach是未知的itemGenerator,ForEach的item builder将执行,创建新的Child组件实例。
- Child({ label: ViewChild this.arrA[last], info: this.arrA[this.arrA.length-1] }):数组的最后一项有更改,因此引起第二个Child的实例的更改。对于Child({ label: ViewChild this.arrA[first], info: this.arrA[0] }),数组的更改并没有触发一个数组项更改的改变,所以第一个Child不会刷新。
this.arrA[Math.floor(this.arrA.length/2)].info:@State无法观察到第二层的变化,但是Info被@Observed装饰,Info的属性的变化将被@ObjectLink观察到。
九、二维数组
使用@Observed观察二维数组的变化。可以声明一个被@Observed装饰的继承Array的子类。
@Observed
class StringArray extends Array<string> {
}
使用new StringArray()来构造StringArray的实例,new运算符使得@Observed生效,@Observed观察到StringArray的属性变化。
声明一个从Array扩展的类class StringArray extends Array {},并创建StringArray的实例。@Observed装饰的类需要使用new运算符来构建class实例。
@Observed
class StringArray extends Array<string> {
}@Component
struct ItemPage {@ObjectLink itemArr: StringArray;build() {Row() {Text('ItemPage').width(100).height(100)ForEach(this.itemArr,(item: string | Resource) => {Text(item).width(100).height(100)},(item: string) => item)}}
}@Entry
@Component
struct IndexPage {@State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()];build() {Column() {ItemPage({ itemArr: this.arr[0] })ItemPage({ itemArr: this.arr[1] })ItemPage({ itemArr: this.arr[2] })Divider()ForEach(this.arr,(itemArr: StringArray) => {ItemPage({ itemArr: itemArr })},(itemArr: StringArray) => itemArr[0])Divider()Button('update').onClick(() => {console.error('Update all items in arr');if ((this.arr[0] as StringArray)[0] !== undefined) {// 正常情况下需要有一个真实的ID来与ForEach一起使用,但此处没有// 因此需要确保推送的字符串是唯一的。this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);} else {this.arr[0].push('Hello');this.arr[1].push('World');this.arr[2].push('!');}})}}
}
效果图
十、继承Map类
说明
从API version 11开始,@ObjectLink支持@Observed装饰Map类型和继承Map类的类型。
在下面的示例中,myMap类型为MyMap<number, string>,点击Button改变myMap的属性,视图会随之刷新。
@Observed
class Info {public info: MyMap<number, string>;constructor(info: MyMap<number, string>) {this.info = info;}
}@Observed
export class MyMap<K, V> extends Map<K, V> {public name: string;constructor(name?: string, args?: [K, V][]) {super(args);this.name = name ? name : "My Map";}getName() {return this.name;}
}@Entry
@Component
struct MapSampleNested {@State message: Info = new Info(new MyMap("myMap", [[0, "a"], [1, "b"], [3, "c"]]));build() {Row() {Column() {MapSampleNestedChild({ myMap: this.message.info })}.width('100%')}.height('100%')}
}@Component
struct MapSampleNestedChild {@ObjectLink myMap: MyMap<number, string>build() {Row() {Column() {ForEach(Array.from(this.myMap.entries()), (item: [number, string]) => {Text(`${item[0]}`).fontSize(30)Text(`${item[1]}`).fontSize(30)Divider().strokeWidth(5)})Button('set new one').width(200).margin(10).onClick(() => {this.myMap.set(4, "d")})Button('clear').width(200).margin(10).onClick(() => {this.myMap.clear()})Button('replace the first one').width(200).margin(10).onClick(() => {this.myMap.set(0, "aa")})Button('delete the first one').width(200).margin(10).onClick(() => {this.myMap.delete(0)})}.width('100%')}.height('100%')}
}
效果图
十一、继承Set类
说明
从API version 11开始,@ObjectLink支持@Observed装饰Set类型和继承Set类的类型。
在下面的示例中,mySet类型为MySet,点击Button改变mySet的属性,视图会随之刷新。
@Observed
class Info {public info: MySet<number>;constructor(info: MySet<number>) {this.info = info;}
}@Observed
export class MySet<T> extends Set<T> {public name: string;constructor(name?: string, args?: T[]) {super(args);this.name = name ? name : "My Set";}getName() {return this.name;}
}@Entry
@Component
struct SetSampleNested {@State message: Info = new Info(new MySet("Set", [0, 1, 2, 3, 4]));build() {Row() {Column() {SetSampleNestedChild({ mySet: this.message.info })}.width('100%')}.height('100%')}
}@Component
struct SetSampleNestedChild {@ObjectLink mySet: MySet<number>build() {Row() {Column() {ForEach(Array.from(this.mySet.entries()), (item: [number, number]) => {Text(`${item}`).fontSize(30)Divider()})Button('set new one').width(200).margin(10).onClick(() => {this.mySet.add(5)})Button('clear').width(200).margin(10).onClick(() => {this.mySet.clear()})Button('delete the first one').width(200).margin(10).onClick(() => {this.mySet.delete(0)})}.width('100%')}.height('100%')}
}
效果图
十二、ObjectLink支持联合类型
@ObjectLink支持@Observed装饰类和undefined或null组成的联合类型,在下面的示例中,count类型为Source | Data | undefined,点击父组件Parent中的Button改变count的属性或者类型,Child中也会对应刷新。
@Observed
class Source {public source: number;constructor(source: number) {this.source = source;}
}@Observed
class Data {public data: number;constructor(data: number) {this.data = data;}
}@Entry
@Component
struct Parent {@State count: Source | Data | undefined = new Source(10)build() {Column() {Child({ count: this.count })Button('change count property').onClick(() => {// 判断count的类型,做属性的更新if (this.count instanceof Source) {this.count.source += 1} else if (this.count instanceof Data) {this.count.data += 1} else {console.info('count is undefined, cannot change property')}})Button('change count to Source').onClick(() => {// 赋值为Source的实例this.count = new Source(100)})Button('change count to Data').onClick(() => {// 赋值为Data的实例this.count = new Data(100)})Button('change count to undefined').onClick(() => {// 赋值为undefinedthis.count = undefined})}.width('100%')}
}@Component
struct Child {@ObjectLink count: Source | Data | undefinedbuild() {Column() {Text(`count is instanceof ${this.count instanceof Source ? 'Source' :this.count instanceof Data ? 'Data' : 'undefined'}`).fontSize(30)Text(`count's property is ${this.count instanceof Source ? this.count.source : this.count?.data}`).fontSize(15)}.width('100%')}
}
效果图