上文所述的裝飾器僅能觀察到第一層的變化,但是在實際應用開發中,應用會根據開發需要,封裝自己的資料模型。對於多層巢狀的情況,比如二維陣列,或者陣列項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可能會帶來問題。
- @ObjectLink裝飾器不能在@Entry裝飾的自定義元件中使用。
裝飾器說明
@Observed類裝飾器 |
說明 |
---|---|
裝飾器引數 |
無 |
類裝飾器 |
裝飾class。需要放在class的定義前,使用new建立類物件。 |
@ObjectLink變數裝飾器 |
說明 |
---|---|
裝飾器引數 |
無 |
同步型別 |
不與父元件中的任何型別同步變數。 |
允許裝飾的變數型別 |
必須為被@Observed裝飾的class例項,必須指定型別。 不支援簡單型別,可以使用@Prop。 @ObjectLink的屬性是可以改變的,但是變數的分配是不允許的,也就是說這個裝飾器裝飾變數是隻讀的,不能被改變。 |
被裝飾變數的初始值 |
不允許。 |
@ObjectLink裝飾的資料為可讀示例。
// 允許@ObjectLink裝飾的資料屬性賦值 this.objLink.a= ... // 不允許@ObjectLink裝飾的資料自身賦值 this.objLink= ...
@ObjectLink裝飾的變數不能被賦值,如果要使用賦值操作,請使用@Prop。
- @Prop裝飾的變數和資料來源的關係是是單向同步,@Prop裝飾的變數在本地複製了資料來源,所以它允許本地更改,如果父元件中的資料來源有更新,@Prop裝飾的變數本地的修改將被覆蓋;
- @ObjectLink裝飾的變數和資料來源的關係是雙向同步,@ObjectLink裝飾的變數相當於指向資料來源的指標。如果一旦發生@ObjectLink裝飾的變數的賦值,則同步鏈將被打斷。
變數的傳遞/訪問規則說明
@ObjectLink傳遞/訪問 |
說明 |
---|---|
從父元件初始化 |
必須指定。 初始化@ObjectLink裝飾的變數必須同時滿足以下場景:
同步源是陣列項的示例請參考物件陣列。初始化的class的示例請參考巢狀物件。 |
與源物件同步 |
雙向。 |
可以初始化子元件 |
允許,可用於初始化常規變數、@State、@Link、@Prop、@Provide |
觀察變化和行為表現
觀察的變化
@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的屬性的變化,示例請參考物件陣列。
框架行為
- 初始渲染:
- @Observed裝飾的class的例項會被不透明的代理物件包裝,代理了class上的屬性的setter和getter方法
- 子元件中@ObjectLink裝飾的從父元件初始化,接收被@Observed裝飾的class的例項,@ObjectLink的包裝類會將自己註冊給@Observed class。
- 屬性更新:當@Observed裝飾的class屬性改變時,會走到代理的setter和getter,然後遍歷依賴它的@ObjectLink包裝類,通知資料更新。
使用場景
巢狀物件
// 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(`ViewC [${this.label}] this.bag.size = ${this.bag.size}`) .fontColor('#ffffffff') .backgroundColor('#ff3fc4c4') .width(320) .height(50) .borderRadius(25) .margin(10) .textAlign(TextAlign.Center) Button(`ViewA: this.bag.size add 1`) .width(320) .backgroundColor('#ff7fcf58') .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('#ff3fc4c4') .width(320) .height(50) .borderRadius(25) .margin(10) .textAlign(TextAlign.Center) Button(`ViewC: this.bookName.size add 1`) .width(320) .backgroundColor('#ff7fcf58') .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(`ViewC: this.child.bookName.size add 10`) .width(320) .backgroundColor('#ff7fcf58') .margin(10) .onClick(() => { this.child.bookName.size += 10 console.log('this.child.bookName.size:' + this.child.bookName.size) }) Button(`ViewB: this.user.bag = new Bag(10)`) .width(320) .backgroundColor('#ff7fcf58') .margin(10) .onClick(() => { this.user.bag = new Bag(10); }) Button(`ViewB: this.user = new User(new Bag(20))`) .width(320) .backgroundColor('#ff7fcf58') .margin(10) .onClick(() => { this.user = new User(new Bag(20)); }) } } }
ViewB中的事件控制代碼:
- this.user.bag = new Bag(10) 和this.user = new User(new Bag(20)): 對@State裝飾的變數b和其屬性的修改。
- this.child.bookName.size += ... :該變化屬於第二層的變化,@State無法觀察到第二層的變化,但是ClassA被@Observed裝飾,ClassA的屬性c的變化可以被@ObjectLink觀察到。
ViewA中的事件控制代碼:
- this.bookName.size += 1:對@ObjectLink變數a的修改,將觸發Button元件的重新整理。@ObjectLink和@Prop不同,@ObjectLink不複製來自父元件的資料來源,而是在本地構建了指向其資料來源的引用。
- @ObjectLink變數是隻讀的,this.bookName = new bookName(...)是不允許的,因為一旦賦值操作發生,指向資料來源的引用將被重置,同步將被打斷。
}
物件陣列
物件陣列是一種常用的資料結構。以下示例展示了陣列物件的用法。
let NextID: number = 1; @Observed class ClassA { public id: number; public c: number; constructor(c: number) { this.id = NextID++; this.c = c; } } @Component struct ViewA { // 子元件ViewA的@ObjectLink的型別是ClassA @ObjectLink a: ClassA; label: string = 'ViewA1'; build() { Row() { Button(`ViewA [${this.label}] this.a.c = ${this.a ? this.a.c : "undefined"}`) .width(320) .margin(10) .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`) .width(320) .margin(10) .onClick(() => { this.arrA = [new ClassA(0), new ClassA(0)]; }) Button(`ViewB: push`) .width(320) .margin(10) .onClick(() => { this.arrA.push(new ClassA(0)) }) Button(`ViewB: shift`) .width(320) .margin(10) .onClick(() => { if (this.arrA.length > 0) { this.arrA.shift() } else { console.log("length <= 0") } }) Button(`ViewB: chg item property in middle`) .width(320) .margin(10) .onClick(() => { this.arrA[Math.floor(this.arrA.length / 2)].c = 10; }) Button(`ViewB: chg item property in middle`) .width(320) .margin(10) .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[last], a: this.arrA[this.arrA.length-1] }):上述更改改變了陣列中第二個元素,所以繫結this.arrA[1]的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 => { Text(item) .width(100).height(100) }, item => 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 => { ItemPage({ itemArr: itemArr }) }, itemArr => itemArr[0] ) Divider() Button('update') .onClick(() => { console.error('Update all items in arr'); if (this.arr[0][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('!'); } }) } } }