鸿蒙-@Observed装饰器和@ObjectLink装饰器

335 阅读4分钟

@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:

  • 被@Observed装饰的类,可以被观察到属性的变化;
  • 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
  • @Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用,如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用。

限制条件

  • 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
  • @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。

@Observed类装饰器

  • 装饰class。需要放在class的定义前,使用new创建类对象。

@ObjectLink变量装饰器

  • @ObjectLink的属性是可以改变的,但是变量的分配是不允许的,也就是说这个装饰器装饰变量是只读的,不能被改变。

  • 不允许初始化

@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。

  • @Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;
  • @ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。禁止对@ObjectLink装饰的变量赋值,如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。

image.png

框架行为

  1. 初始渲染:
    1. @Observed装饰的class的实例会被不透明的代理对象包装,代理了class上的属性的setter和getter方法
    2. 子组件中@ObjectLink装饰的从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类会将自己注册给@Observed class。
  2. 属性更新:当@Observed装饰的class属性改变时,会走到代理的setter和getter,然后遍历依赖它的@ObjectLink包装类,通知数据更新。

使用场景

二维数组

@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('!');
          }
        })
    }
  }
}

常见问题

在子组件中给@ObjectLink装饰的变量赋值

在子组件中给@ObjectLink装饰的变量赋值是不允许的。

对于实现双向数据同步的@ObjectLink,赋值相当于要更新父组件中的数组项或者class的属性,这个对于 TypeScript/JavaScript是不能实现的。框架对于这种行为会发生运行时报错。

@Observed
class ClassA {
  public c: number = 0;

  constructor(c: number) {
    this.c = c;
  }
}

@Component
struct ObjectLinkChild {
  @ObjectLink testNum: ClassA;

  build() {
    Text(`ObjectLinkChild testNum ${this.testNum.c}`)
      .onClick(() => {
        // 可以对ObjectLink装饰对象的属性赋值
        this.testNum.c = 47;
      })
  }
}

@Entry
@Component
struct Parent {
  @State testNum: ClassA[] = [new ClassA(1)];

  build() {
    Column() {
      Text(`Parent testNum ${this.testNum[0].c}`)
        .onClick(() => {
          this.testNum[0].c += 1;
        })
        
      ObjectLinkChild({ testNum: this.testNum[0] })
    }
  }
}

基础嵌套对象属性更改失效

@State b : ClassB 只能观察到this.b属性的变化,比如this.b.a, this.b.b 和this.b.c的变化,但是无法观察嵌套在属性中的属性,即this.b.c.c(属性c是内嵌在b中的对象classC的属性)。

  • 为了观察到嵌套于内部的ClassC的属性,需要做如下改变:

    • 构造一个子组件,用于单独渲染ClassC的实例。 该子组件可以使用@ObjectLink c : ClassC或@Prop c : ClassC。通常会使用@ObjectLink,除非子组件需要对其ClassC对象进行本地修改。
    • 嵌套的ClassC必须用@Observed装饰。当在ClassB中创建ClassC对象时,它将被包装在ES6代理中,当ClassC属性更改时(this.b.c.c += 1),该代码将修改通知到@ObjectLink变量。
class ClassA {
  a: number;

  constructor(a: number) {
    this.a = a;
  }

  getA(): number {
    return this.a;
  }

  setA(a: number): void {
    this.a = a;
  }
}

@Observed
class ClassC {
  c: number;

  constructor(c: number) {
    this.c = c;
  }

  getC(): number {
    return this.c;
  }

  setC(c: number): void {
    this.c = c;
  }
}

class ClassB extends ClassA {
  b: number = 47;
  c: ClassC;

  constructor(a: number, b: number, c: number) {
    super(a);
    this.b = b;
    this.c = new ClassC(c);
  }

  getB(): number {
    return this.b;
  }

  setB(b: number): void {
    this.b = b;
  }

  getC(): number {
    return this.c.getC();
  }

  setC(c: number): void {
    return this.c.setC(c);
  }
}

@Component
struct ViewClassC {
  @ObjectLink c: ClassC;

  build() {
    Column({ space: 10 }) {
      Text(`c: ${this.c.getC()}`)
      Button("Change C")
        .onClick(() => {
          this.c.setC(this.c.getC() + 1);
        })
    }
  }
}

@Entry
@Component
struct MyView {
  @State b: ClassB = new ClassB(10, 20, 30);

  build() {
    Column({ space: 10 }) {
      Text(`a: ${this.b.a}`)
      Button("Change ClassA.a")
        .onClick(() => {
          this.b.a += 1;
        })

      Text(`b: ${this.b.b}`)
      Button("Change ClassB.b")
        .onClick(() => {
          this.b.b += 1;
        })

      ViewClassC({ c: this.b.c }) // Text(`c: ${this.b.c.c}`)的替代写法
      Button("Change ClassB.ClassC.c")
        .onClick(() => {
          this.b.c.c += 1;
        })
    }
  }
}