@Observed裝飾器和@ObjectLink裝飾器:嵌套類對象屬性變化

2024-01-25 12:05 更新

上文所述的裝飾器僅能觀察到第一層的變化,但是在實際應用開發(fā)中,應用會根據(jù)開發(fā)需要,封裝自己的數(shù)據(jù)模型。對于多層嵌套的情況,比如二維數(shù)組,或者數(shù)組項class,或者class的屬性是class,他們的第二層的屬性變化是無法觀察到的。這就引出了@Observed/@ObjectLink裝飾器。

說明

從API version 9開始,這兩個裝飾器支持在ArkTS卡片中使用。

概述

@ObjectLink和@Observed類裝飾器用于在涉及嵌套對象或數(shù)組的場景中進行雙向數(shù)據(jù)同步:

  • 被@Observed裝飾的類,可以被觀察到屬性的變化;
  • 子組件中@ObjectLink裝飾器裝飾的狀態(tài)變量用于接收@Observed裝飾的類的實例,和父組件中對應的狀態(tài)變量建立雙向數(shù)據(jù)綁定。這個實例可以是數(shù)組中的被@Observed裝飾的項,或者是class object中的屬性,這個屬性同樣也需要被@Observed裝飾。
  • 單獨使用@Observed是沒有任何作用的,需要搭配@ObjectLink或者@Prop使用。

限制條件

  • 使用@Observed裝飾class會改變class原始的原型鏈,@Observed和其他類裝飾器裝飾同一個class可能會帶來問題。
  • @ObjectLink裝飾器不能在@Entry裝飾的自定義組件中使用。

裝飾器說明

@Observed類裝飾器

說明

裝飾器參數(shù)

類裝飾器

裝飾class。需要放在class的定義前,使用new創(chuàng)建類對象。

@ObjectLink變量裝飾器

說明

裝飾器參數(shù)

同步類型

不與父組件中的任何類型同步變量。

允許裝飾的變量類型

必須為被@Observed裝飾的class實例,必須指定類型。

不支持簡單類型,可以使用@Prop。

@ObjectLink的屬性是可以改變的,但是變量的分配是不允許的,也就是說這個裝飾器裝飾變量是只讀的,不能被改變。

被裝飾變量的初始值

不允許。

@ObjectLink裝飾的數(shù)據(jù)為可讀示例。

  1. // 允許@ObjectLink裝飾的數(shù)據(jù)屬性賦值
  2. this.objLink.a= ...
  3. // 不允許@ObjectLink裝飾的數(shù)據(jù)自身賦值
  4. this.objLink= ...
說明

@ObjectLink裝飾的變量不能被賦值,如果要使用賦值操作,請使用@Prop

  • @Prop裝飾的變量和數(shù)據(jù)源的關系是是單向同步,@Prop裝飾的變量在本地拷貝了數(shù)據(jù)源,所以它允許本地更改,如果父組件中的數(shù)據(jù)源有更新,@Prop裝飾的變量本地的修改將被覆蓋;
  • @ObjectLink裝飾的變量和數(shù)據(jù)源的關系是雙向同步,@ObjectLink裝飾的變量相當于指向數(shù)據(jù)源的指針。如果一旦發(fā)生@ObjectLink裝飾的變量的賦值,則同步鏈將被打斷。

變量的傳遞/訪問規(guī)則說明

@ObjectLink傳遞/訪問

說明

從父組件初始化

必須指定。

初始化@ObjectLink裝飾的變量必須同時滿足以下場景:

  • 類型必須是@Observed裝飾的class。
  • 初始化的數(shù)值需要是數(shù)組項,或者class的屬性。
  • 同步源的class或者數(shù)組必須是@State,@Link,@Provide,@Consume或者@ObjectLink裝飾的數(shù)據(jù)。

同步源是數(shù)組項的示例請參考對象數(shù)組。初始化的class的示例請參考嵌套對象。

與源對象同步

雙向。

可以初始化子組件

允許,可用于初始化常規(guī)變量、@State、@Link、@Prop、@Provide

圖1 初始化規(guī)則圖示

觀察變化和行為表現(xiàn)

觀察的變化

@Observed裝飾的類,如果其屬性為非簡單類型,比如class、Object或者數(shù)組,也需要被@Observed裝飾,否則將觀察不到其屬性的變化。

  1. class ClassA {
  2. public c: number;
  3. constructor(c: number) {
  4. this.c = c;
  5. }
  6. }
  7. @Observed
  8. class ClassB {
  9. public a: ClassA;
  10. public b: number;
  11. constructor(a: ClassA, b: number) {
  12. this.a = a;
  13. this.b = b;
  14. }
  15. }

以上示例中,ClassB被@Observed裝飾,其成員變量的賦值的變化是可以被觀察到的,但對于ClassA,沒有被@Observed裝飾,其屬性的修改不能被觀察到。

  1. @ObjectLink b: ClassB
  2. // 賦值變化可以被觀察到
  3. this.b.a = new ClassA(5)
  4. this.b.b = 5
  5. // ClassA沒有被@Observed裝飾,其屬性的變化觀察不到
  6. this.b.a.c = 5

@ObjectLink:@ObjectLink只能接收被@Observed裝飾class的實例,可以觀察到:

  • 其屬性的數(shù)值的變化,其中屬性是指Object.keys(observedObject)返回的所有屬性,示例請參考嵌套對象。
  • 如果數(shù)據(jù)源是數(shù)組,則可以觀察到數(shù)組item的替換,如果數(shù)據(jù)源是class,可觀察到class的屬性的變化,示例請參考對象數(shù)組。

框架行為

  1. 初始渲染:
    1. @Observed裝飾的class的實例會被不透明的代理對象包裝,代理了class上的屬性的setter和getter方法
    2. 子組件中@ObjectLink裝飾的從父組件初始化,接收被@Observed裝飾的class的實例,@ObjectLink的包裝類會將自己注冊給@Observed class。
  2. 屬性更新:當@Observed裝飾的class屬性改變時,會走到代理的setter和getter,然后遍歷依賴它的@ObjectLink包裝類,通知數(shù)據(jù)更新。

使用場景

嵌套對象

以下是嵌套類對象的數(shù)據(jù)結構。

  1. // objectLinkNestedObjects.ets
  2. let NextID: number = 1;
  3. @Observed
  4. class ClassA {
  5. public id: number;
  6. public c: number;
  7. constructor(c: number) {
  8. this.id = NextID++;
  9. this.c = c;
  10. }
  11. }
  12. @Observed
  13. class ClassB {
  14. public a: ClassA;
  15. constructor(a: ClassA) {
  16. this.a = a;
  17. }
  18. }
以下組件層次結構呈現(xiàn)的是嵌套類對象的數(shù)據(jù)結構。
  1. @Component
  2. struct ViewA {
  3. label: string = 'ViewA1';
  4. @ObjectLink a: ClassA;
  5. build() {
  6. Row() {
  7. Button(`ViewA [${this.label}] this.a.c=${this.a.c} +1`)
  8. .onClick(() => {
  9. this.a.c += 1;
  10. })
  11. }
  12. }
  13. }
  14. @Entry
  15. @Component
  16. struct ViewB {
  17. @State b: ClassB = new ClassB(new ClassA(0));
  18. build() {
  19. Column() {
  20. // in low version,DevEco may throw a warning,but it does not matter.
  21. // you can still compile and run.
  22. ViewA({ label: 'ViewA #1', a: this.b.a })
  23. ViewA({ label: 'ViewA #2', a: this.b.a })
  24. Button(`ViewB: this.b.a.c+= 1`)
  25. .onClick(() => {
  26. this.b.a.c += 1;
  27. })
  28. Button(`ViewB: this.b.a = new ClassA(0)`)
  29. .onClick(() => {
  30. this.b.a = new ClassA(0);
  31. })
  32. Button(`ViewB: this.b = new ClassB(ClassA(0))`)
  33. .onClick(() => {
  34. this.b = new ClassB(new ClassA(0));
  35. })
  36. }
  37. }
  38. }

ViewB中的事件句柄:

  • this.b.a = new ClassA(0) 和this.b = new ClassB(new ClassA(0)): 對@State裝飾的變量b和其屬性的修改。
  • this.b.a.c = ... :該變化屬于第二層的變化,@State無法觀察到第二層的變化,但是ClassA被@Observed裝飾,ClassA的屬性c的變化可以被@ObjectLink觀察到。

ViewA中的事件句柄:

  • this.a.c += 1:對@ObjectLink變量a的修改,將觸發(fā)Button組件的刷新。@ObjectLink和@Prop不同,@ObjectLink不拷貝來自父組件的數(shù)據(jù)源,而是在本地構建了指向其數(shù)據(jù)源的引用。
  • @ObjectLink變量是只讀的,this.a = new ClassA(...)是不允許的,因為一旦賦值操作發(fā)生,指向數(shù)據(jù)源的引用將被重置,同步將被打斷。

對象數(shù)組

對象數(shù)組是一種常用的數(shù)據(jù)結構。以下示例展示了數(shù)組對象的用法。

  1. @Component
  2. struct ViewA {
  3. // 子組件ViewA的@ObjectLink的類型是ClassA
  4. @ObjectLink a: ClassA;
  5. label: string = 'ViewA1';
  6. build() {
  7. Row() {
  8. Button(`ViewA [${this.label}] this.a.c = ${this.a.c} +1`)
  9. .onClick(() => {
  10. this.a.c += 1;
  11. })
  12. }
  13. }
  14. }
  15. @Entry
  16. @Component
  17. struct ViewB {
  18. // ViewB中有@State裝飾的ClassA[]
  19. @State arrA: ClassA[] = [new ClassA(0), new ClassA(0)];
  20. build() {
  21. Column() {
  22. ForEach(this.arrA,
  23. (item) => {
  24. ViewA({ label: `#${item.id}`, a: item })
  25. },
  26. (item) => item.id.toString()
  27. )
  28. // 使用@State裝飾的數(shù)組的數(shù)組項初始化@ObjectLink,其中數(shù)組項是被@Observed裝飾的ClassA的實例
  29. ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] })
  30. ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] })
  31. Button(`ViewB: reset array`)
  32. .onClick(() => {
  33. this.arrA = [new ClassA(0), new ClassA(0)];
  34. })
  35. Button(`ViewB: push`)
  36. .onClick(() => {
  37. this.arrA.push(new ClassA(0))
  38. })
  39. Button(`ViewB: shift`)
  40. .onClick(() => {
  41. this.arrA.shift()
  42. })
  43. Button(`ViewB: chg item property in middle`)
  44. .onClick(() => {
  45. this.arrA[Math.floor(this.arrA.length / 2)].c = 10;
  46. })
  47. Button(`ViewB: chg item property in middle`)
  48. .onClick(() => {
  49. this.arrA[Math.floor(this.arrA.length / 2)] = new ClassA(11);
  50. })
  51. }
  52. }
  53. }
  • this.arrA[Math.floor(this.arrA.length/2)] = new ClassA(..) :該狀態(tài)變量的改變觸發(fā)2次更新:
    1. ForEach:數(shù)組項的賦值導致ForEach的itemGenerator被修改,因此數(shù)組項被識別為有更改,F(xiàn)orEach的item builder將執(zhí)行,創(chuàng)建新的ViewA組件實例。
    2. ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }):上述更改改變了數(shù)組中第二個元素,所以綁定this.arrA[1]的ViewA將被更新;
  • this.arrA.push(new ClassA(0)) : 將觸發(fā)2次不同效果的更新:
    1. ForEach:新添加的ClassA對象對于ForEach是未知的itemGenerator,F(xiàn)orEach的item builder將執(zhí)行,創(chuàng)建新的ViewA組件實例。
    2. ViewA({ label: `ViewA this.arrA[last]`, a: this.arrA[this.arrA.length-1] }):數(shù)組的最后一項有更改,因此引起第二個ViewA的實例的更改。對于ViewA({ label: `ViewA this.arrA[first]`, a: this.arrA[0] }),數(shù)組的更改并沒有觸發(fā)一個數(shù)組項更改的改變,所以第一個ViewA不會刷新。
  • this.arrA[Math.floor(this.arrA.length/2)].c:@State無法觀察到第二層的變化,但是ClassA被@Observed裝飾,ClassA的屬性的變化將被@ObjectLink觀察到。

二維數(shù)組

使用@Observed觀察二維數(shù)組的變化??梢月暶饕粋€被@Observed裝飾的繼承Array的子類。

  1. @Observed
  2. class StringArray extends Array<String> {
  3. }

使用new StringArray()來構造StringArray的實例,new運算符使得@Observed生效,@Observed觀察到StringArray的屬性變化。

聲明一個從Array擴展的類class StringArray extends Array<String> {},并創(chuàng)建StringArray的實例。@Observed裝飾的類需要使用new運算符來構建class實例。

  1. @Observed
  2. class StringArray extends Array<String> {
  3. }
  4. @Component
  5. struct ItemPage {
  6. @ObjectLink itemArr: StringArray;
  7. build() {
  8. Row() {
  9. Text('ItemPage')
  10. .width(100).height(100)
  11. ForEach(this.itemArr,
  12. item => {
  13. Text(item)
  14. .width(100).height(100)
  15. },
  16. item => item
  17. )
  18. }
  19. }
  20. }
  21. @Entry
  22. @Component
  23. struct IndexPage {
  24. @State arr: Array<StringArray> = [new StringArray(), new StringArray(), new StringArray()];
  25. build() {
  26. Column() {
  27. ItemPage({ itemArr: this.arr[0] })
  28. ItemPage({ itemArr: this.arr[1] })
  29. ItemPage({ itemArr: this.arr[2] })
  30. Divider()
  31. ForEach(this.arr,
  32. itemArr => {
  33. ItemPage({ itemArr: itemArr })
  34. },
  35. itemArr => itemArr[0]
  36. )
  37. Divider()
  38. Button('update')
  39. .onClick(() => {
  40. console.error('Update all items in arr');
  41. if (this.arr[0][0] !== undefined) {
  42. // 正常情況下需要有一個真實的ID來與ForEach一起使用,但此處沒有
  43. // 因此需要確保推送的字符串是唯一的。
  44. this.arr[0].push(`${this.arr[0].slice(-1).pop()}${this.arr[0].slice(-1).pop()}`);
  45. this.arr[1].push(`${this.arr[1].slice(-1).pop()}${this.arr[1].slice(-1).pop()}`);
  46. this.arr[2].push(`${this.arr[2].slice(-1).pop()}${this.arr[2].slice(-1).pop()}`);
  47. } else {
  48. this.arr[0].push('Hello');
  49. this.arr[1].push('World');
  50. this.arr[2].push('!');
  51. }
  52. })
  53. }
  54. }
  55. }
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號