上文所述的裝飾器僅能觀察到第一層的變化,但是在實際應用開發中,應用會根據開發需要,封裝自己的數據模型。對于多層嵌套的情況,比如二維數組,或者數組項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裝飾的數據為可讀示例。
// 允許@ObjectLink裝飾的數據屬性賦值
this.objLink.a= ...
// 不允許@ObjectLink裝飾的數據自身賦值
this.objLink= ...
說明:
@ObjectLink裝飾的變量不能被賦值,如果要使用賦值操作,請使用 @Prop 。
●?@Prop裝飾的變量和數據源的關系是是單向同步,@Prop裝飾的變量在本地拷貝了數據源,所以它允許本地更改,如果父組件中的數據源有更新,@Prop裝飾的變量本地的修改將被覆蓋;
●?@ObjectLink裝飾的變量和數據源的關系是雙向同步,@ObjectLink裝飾的變量相當于指向數據源的指針。禁止對@ObjectLink裝飾的變量賦值,如果一旦發生@ObjectLink裝飾的變量的賦值,則同步鏈將被打斷。因為@ObjectLink修飾的變量通過數據源(Object)引用來初始化。對于實現雙向數據同步的@ObjectLink,賦值相當于更新父組件中的數組項或者class的屬性,TypeScript/JavaScript不能實現,會發生運行時報錯。
變量的傳遞/訪問規則說明
圖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 += 10console.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) 無法觀察到第二層的變化,但是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?{},并創建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('!');}})}}
}
如果大家想更加深入的學習 OpenHarmony 開發的內容,不妨可以參考以下相關學習文檔進行學習,助你快速提升自己:
OpenHarmony 開發環境搭建:https://qr18.cn/CgxrRy
《OpenHarmony源碼解析》:https://qr18.cn/CgxrRy
- 搭建開發環境
- Windows 開發環境的搭建
- Ubuntu 開發環境搭建
- Linux 與 Windows 之間的文件共享
- ……
系統架構分析:https://qr18.cn/CgxrRy
- 構建子系統
- 啟動流程
- 子系統
- 分布式任務調度子系統
- 分布式通信子系統
- 驅動子系統
- ……