openharmony嵌套类对象属性变化:@observed装饰器和@objectlink装饰器
上文所述的装饰器仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@observed/@objectlink装饰器。
说明:
从api version 9开始,这两个装饰器支持在arkts卡片中使用。
概述
@objectlink和@observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
● 被@observed装饰的类,可以被观察到属性的变化;
● 子组件中@objectlink装饰器装饰的状态变量用于接收@observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@observed装饰的项,或者是class object中的属性,这个属性同样也需要被@observed装饰。
● 单独使用@observed是没有任何作用的,需要搭配@objectlink或者@prop使用。
限制条件
使用@observed装饰class会改变class原始的原型链,@observed和其他类装饰器装饰同一个class可能会带来问题。
装饰器说明
@observed类装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
@objectlink变量装饰器 | 说明 |
---|---|
装饰器参数 | 无 |
同步类型 | 不与父组件中的任何类型同步变量。 |
允许装饰的变量类型 | 必须为被@observed装饰的class实例,必须指定类型。不支持简单类型,可以使用@prop。支持继承date或者array的class实例,示例见观察变化。@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 |
图1 初始化规则图示
观察变化和行为表现
观察变化
@observed装饰的类,如果其属性为非简单类型,比如class、object或者数组,也需要被@observed装饰,否则将观察不到其属性的变化。
class classa {
public c: number;
constructor(c: number) {
this.c = c;
}
}
@observed
class classb {
public a: classa;
public b: number;
constructor(a: classa, b: number) {
this.a = a;
this.b = b;
}
}
以上示例中,classb被@observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于classa,没有被@observed装饰,其属性的修改不能被观察到。
@objectlink b: classb
// 赋值变化可以被观察到
this.b.a = new classa(5)
this.b.b = 5
// classa没有被@observed装饰,其属性的变化观察不到
this.b.a.c = 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 classb {
public a: dateclass;
constructor(a: dateclass) {
this.a = a;
}
}
@component
struct viewa {
label: string = 'date';
@objectlink a: dateclass;
build() {
column() {
button(`child increase the day by 1`)
.onclick(() => {
this.a.setdate(this.a.getdate() + 1);
})
datepicker({
start: new date('1970-1-1'),
end: new date('2100-1-1'),
selected: this.a
})
}
}
}
@entry
@component
struct viewb {
@state b: classb = new classb(new dateclass('2023-1-1'));
build() {
column() {
viewa({ label: 'date', a: this.b.a })
button(`parent update the new date`)
.onclick(() => {
this.b.a = new dateclass('2023-07-07');
})
button(`viewb: this.b = new classb(new dateclass('2023-08-20'))`)
.onclick(() => {
this.b = new classb(new dateclass('2023-08-20'));
})
}
}
}
框架行为
1. 初始渲染:
a. @observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
b. 子组件中@objectlink装饰的从父组件初始化,接收被@observed装饰的class的实例,@objectlink的包装类会将自己注册给@observed class。
2. 属性更新:当@observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@objectlink包装类,通知数据更新。
使用场景
嵌套对象
以下是嵌套类对象的数据结构。
// objectlinknestedobjects.ets
let nextid: number = 1;
@observed
class classa {
public id: number;
public c: number;
constructor(c: number) {
this.id = nextid++;
this.c = c;
}
}
@observed
class classb {
public a: classa;
constructor(a: classa) {
this.a = a;
}
}
@observed
class classd {
public c: classc;
constructor(c: classc) {
this.c = c;
}
}
@observed
class classc extends classa {
public k: number;
constructor(k: number) {
// 调用父类方法对k进行处理
super(k);
this.k = k;
}
}
以下组件层次结构呈现的是嵌套类对象的数据结构。
@component
struct viewc {
label: string = 'viewc1';
@objectlink c: classc;
build() {
row() {
column() {
text(`viewc [${this.label}] this.a.c = ${this.c.c}`)
.fontcolor('#ffffffff')
.backgroundcolor('#ff3fc4c4')
.height(50)
.borderradius(25)
button(`viewc: this.c.c add 1`)
.backgroundcolor('#ff7fcf58')
.onclick(() => {
this.c.c += 1;
console.log('this.c.c:' + this.c.c)
})
}
.width(300)
}
}
}
@entry
@component
struct viewb {
@state b: classb = new classb(new classa(0));
@state child : classd = new classd(new classc(0));
build() {
column() {
viewc({ label: 'viewc #3', c: this.child.c})
button(`viewc: this.child.c.c add 10`)
.backgroundcolor('#ff7fcf58')
.onclick(() => {
this.child.c.c += 10
console.log('this.child.c.c:' + this.child.c.c)
})
}
}
}
被@observed装饰的classc类,可以观测到继承基类的属性的变化。
viewb中的事件句柄:
● this.child.c = new classa(0) 和this.b = new classb(new classa(0)): 对@state装饰的变量b和其属性的修改。
● this.child.c.c = ... :该变化属于第二层的变化,@state无法观察到第二层的变化,但是classa被@observed装饰,classa的属性c的变化可以被@objectlink观察到。
viewc中的事件句柄:
● this.c.c += 1:对@objectlink变量a的修改,将触发button组件的刷新。@objectlink和@prop不同,@objectlink不拷贝来自父组件的数据源,而是在本地构建了指向其数据源的引用。
● @objectlink变量是只读的,this.a = new classa(...)是不允许的,因为一旦赋值操作发生,指向数据源的引用将被重置,同步将被打断。
对象数组
对象数组是一种常用的数据结构。以下示例展示了数组对象的用法。
@component
struct viewa {
// 子组件viewa的@objectlink的类型是classa
@objectlink a: classa;
label: string = 'viewa1';
build() {
row() {
button(`viewa [${this.label}] this.a.c = ${this.a.c} +1`)
.onclick(() => {
this.a.c += 1;
})
}
}
}
@entry
@component
struct viewb {
// viewb中有@state装饰的classa[]
@state arra: classa[] = [new classa(0), new classa(0)];
build() {
column() {
foreach(this.arra,
(item: classa) => {
viewa({ label: `#${item.id}`, a: item })
},
(item: classa): string => item.id.tostring()
)
// 使用@state装饰的数组的数组项初始化@objectlink,其中数组项是被@observed装饰的classa的实例
viewa({ label: `viewa this.arra[first]`, a: this.arra[0] })
viewa({ label: `viewa this.arra[last]`, a: this.arra[this.arra.length-1] })
button(`viewb: reset array`)
.onclick(() => {
this.arra = [new classa(0), new classa(0)];
})
button(`viewb: push`)
.onclick(() => {
this.arra.push(new classa(0))
})
button(`viewb: shift`)
.onclick(() => {
this.arra.shift()
})
button(`viewb: chg item property in middle`)
.onclick(() => {
this.arra[math.floor(this.arra.length / 2)].c = 10;
})
button(`viewb: chg item property in middle`)
.onclick(() => {
this.arra[math.floor(this.arra.length / 2)] = new classa(11);
})
}
}
}
● this.arra[math.floor(this.arra.length/2)] = new classa(..) :该状态变量的改变触发2次更新:
○ foreach:数组项的赋值导致foreach的itemgenerator被修改,因此数组项被识别为有更改,foreach的item builder将执行,创建新的viewa组件实例。
○ viewa({ label: viewa this.arra[first], a: this.arra[0] }):上述更改改变了数组中第一个元素,所以绑定this.arra[0]的viewa将被更新。
● this.arra.push(new classa(0)) : 将触发2次不同效果的更新:
○ foreach:新添加的classa对象对于foreach是未知的itemgenerator,foreach的item builder将执行,创建新的viewa组件实例。
○ viewa({ label: viewa this.arra[last], a: this.arra[this.arra.length-1] }):数组的最后一项有更改,因此引起第二个viewa的实例的更改。对于viewa({ label: viewa this.arra[first], a: this.arra[0] }),数组的更改并没有触发一个数组项更改的改变,所以第一个viewa不会刷新。
● this.arra[math.floor(this.arra.length/2)].c:@state无法观察到第二层的变化,但是classa被@observed装饰,classa的属性的变化将被@objectlink观察到。
二维数组
使用@observed观察二维数组的变化。可以声明一个被@observed装饰的继承array的子类。
@observed
class stringarray extends array<string> {
}
使用new stringarray()来构造stringarray的实例,new运算符使得@observed生效,@observed观察到stringarray的属性变化。
声明一个从array扩展的类class stringarray extends array<string> {},并创建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: string) => itemarr[0]
)
divider()
button('update')
.onclick(() => {
console.error('update all items in arr');
if ((this.arr[0] as array<string>)[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('!');
}
})
}
}
}
发表评论