【中秋國慶不斷更】OpenHarmony巢狀類物件屬性變化

OpenHarmony開發者社群發表於2023-10-05

上文所述的裝飾器僅能觀察到第一層的變化,但是在實際應用開發中,應用會根據開發需要,封裝自己的資料模型。對於多層巢狀的情況,比如二維陣列,或者陣列項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('!');
          }
        })
    }
  }
}


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70011554/viewspace-2986393/,如需轉載,請註明出處,否則將追究法律責任。

相關文章