[二、狀態管理]2管理元件擁有的狀態(5)@Observed裝飾器和@ObjectLink裝飾器:巢狀類物件屬性變化

为敢技术發表於2024-07-25

上文所述的裝飾器僅能觀察到第一層的變化,但是在實際應用開發中,應用會根據開發需要,封裝自己的資料模型。對於多層巢狀的情況,比如二維陣列,或者陣列項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裝飾的變數必須同時滿足以下場景:

  • 型別必須是@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的屬性的變化,示例請參考物件陣列

框架行為

  1. 初始渲染:
    1. @Observed裝飾的class的例項會被不透明的代理物件包裝,代理了class上的屬性的setter和getter方法
    2. 子元件中@ObjectLink裝飾的從父元件初始化,接收被@Observed裝飾的class的例項,@ObjectLink的包裝類會將自己註冊給@Observed class。
  2. 屬性更新:當@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次更新:
    1. ForEach:陣列項的賦值導致ForEach的itemGenerator被修改,因此陣列項被識別為有更改,ForEach的item builder將執行,建立新的ViewA元件例項。
    2. ViewA({ label: ViewA this.arrA[last], a: this.arrA[this.arrA.length-1] }):上述更改改變了陣列中第二個元素,所以繫結this.arrA[1]的ViewA將被更新;
  • this.arrA.push(new ClassA(0)) : 將觸發2次不同效果的更新:
    1. ForEach:新新增的ClassA物件對於ForEach是未知的itemGenerator,ForEach的item builder將執行,建立新的ViewA元件例項。
    2. 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('!');
          }
        })
    }
  }
}

相關文章