《深入理解组件间数据同步:@Provide/@Consume与@Observed/@ObjectLink的特性及限制》
文章目录
- @Provide/@Consume状态变量
- 引言
- 概述
- 观察变化
- 限制条件
- @Observed/@ObjecktLink:嵌套类对象属性变化
- 引言
- 概述
- 观察变化
- 限制条件
@Provide/@Consume状态变量
引言
@Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
其中@Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。
概述
- @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。
- 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。
- @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。
// 通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 通过相同的变量别名绑定
@Provide('a') b: number = 0;
@Consume('a') c: number;
@Provide和@Consume通过相同的变量名或者相同的变量别名绑定时,@Provide装饰的变量和@Consume装饰的变量是一对多的关系。不允许在同一个自定义组件内,包括其子组件中声明多个同名或者同别名的@Provide装饰的变量,@Provide的属性名或别名需要唯一且确定,如果声明多个同名或者同别名的@Provide装饰的变量,会发生运行时报错。
@Provide初始化规则图示
** @Consume初始化规则图示**
观察变化
- 当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
- 当装饰的数据类型为class或者Object的时候,可以观察到赋值和属性赋值的变化(属性为Object.keys(observedObject)返回的所有属性)。
- 当装饰的对象是array的时候,可以观察到数组的添加、删除、更新数组单元。
- 当装饰的对象是Date时,可以观察到Date整体的赋值,同时可通过调用Date的接口setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds 更新Date的属性。
@Component
struct CompD {
@Consume selectedDate: Date;
build() {
Column() {
Button(`child increase the day by 1`)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1)
})
Button('child update the new date')
.margin(10)
.onClick(() => {
this.selectedDate = new Date('2023-09-09')
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.selectedDate
})
}
}
}
@Entry
@Component
struct CompA {
@Provide selectedDate: Date = new Date('2021-08-08')
build() {
Column() {
Button('parent increase the day by 1')
.margin(10)
.onClick(() => {
this.selectedDate.setDate(this.selectedDate.getDate() + 1)
})
Button('parent update the new date')
.margin(10)
.onClick(() => {
this.selectedDate = new Date('2023-07-07')
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.selectedDate
})
CompD()
}
}
}
- 当装饰的变量是Map时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。
- 当装饰的变量是Set时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。
限制条件
- @Provider/@Consumer的参数key必须为string类型,否则编译期会报错。
// 错误写法,编译报错
let change: number = 10;
@Provide(change) message: string = 'Hello';
// 正确写法
let change: string = 'change';
@Provide(change) message: string = 'Hello';
- @Consume装饰的变量不能本地初始化,也不能在构造参数中传入初始化,否则编译期会报错。@Consume仅能通过key来匹配对应的@Provide变量进行初始化。
【反例】
@Component
struct Child {
@Consume msg: string;
// 错误写法,不允许本地初始化
@Consume msg1: string = 'Hello';
build() {
Text(this.msg)
}
}
@Entry
@Component
struct Example {
@Provide message: string = 'Hello';
build() {
Column() {
// 错误写法,不允许外部传入初始化
Child({msg: 'Hello'})
}
}
}
【正例】
@Component
struct Child {
@Consume num: number;
build() {
Column() {
Text(`num的值: ${this.num}`)
}
}
}
@Entry
@Component
struct Parent {
@Provide num: number = 10;
build() {
Column() {
Text(`num的值: ${this.num}`)
Child()
}
}
}
- @Provide的key重复定义时,框架会抛出运行时错误,提醒开发者重复定义key,如果开发者需要重复key,可以使用allowoverride。
// 错误写法,a重复定义
@Provide('a') count: number = 10;
@Provide('a') num: number = 10;
// 正确写法
@Provide('a') count: number = 10;
@Provide('b') num: number = 10;
- 在初始化@Consume变量时,如果开发者没有定义对应key的@Provide变量,框架会抛出运行时错误,提示开发者初始化@Consume变量失败,原因是无法找到其对应key的@Provide变量。
【反例】
@Component
struct Child {
@Consume num: number;
build() {
Column() {
Text(`num的值: ${this.num}`)
}
}
}
@Entry
@Component
struct Parent {
// 错误写法,缺少@Provide
num: number = 10;
build() {
Column() {
Text(`num的值: ${this.num}`)
Child()
}
}
}
【正例】
@Component
struct Child {
@Consume num: number;
build() {
Column() {
Text(`num的值: ${this.num}`)
}
}
}
@Entry
@Component
struct Parent {
// 正确写法
@Provide num: number = 10;
build() {
Column() {
Text(`num的值: ${this.num}`)
Child()
}
}
}
- @Provide与@Consume不支持装饰Function类型的变量,框架会抛出运行时错误。
@Observed/@ObjecktLink:嵌套类对象属性变化
引言
@Provide/@Consume状态变量仅能观察到第一层的变化,但是在实际应用开发中,应用会根据开发需要,封装自己的数据模型。对于多层嵌套的情况,比如二维数组,或者数组项class,或者class的属性是class,他们的第二层的属性变化是无法观察到的。这就引出了@Observed/@ObjectLink装饰器。
概述
@ObjectLink和@Observed类装饰器用于在涉及嵌套对象或数组的场景中进行双向数据同步:
- 使用new创建被@Observed装饰的类,可以被观察到属性的变化;
- 子组件中@ObjectLink装饰器装饰的状态变量用于接收@Observed装饰的类的实例,和父组件中对应的状态变量建立双向数据绑定。这个实例可以是数组中的被@Observed装饰的项,或者是class object中的属性,这个属性同样也需要被@Observed装饰。
- @Observed用于嵌套类场景中,观察对象类属性变化,要配合自定义组件使用,如果要做数据双/单向同步,需要搭配@ObjectLink或者@Prop使用。
说明
@ObjectLink装饰的变量不能被赋值,如果要使用赋值操作,请使用@Prop。
- @Prop装饰的变量和数据源的关系是是单向同步,@Prop装饰的变量在本地拷贝了数据源,所以它允许本地更改,如果父组件中的数据源有更新,@Prop装饰的变量本地的修改将被覆盖;
- @ObjectLink装饰的变量和数据源的关系是双向同步,@ObjectLink装饰的变量相当于指向数据源的指针。禁止对@ObjectLink装饰的变量赋值,如果一旦发生@ObjectLink装饰的变量的赋值,则同步链将被打断。因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向数据同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TypeScript/JavaScript不能实现,会发生运行时报错。
初始化规则图示
观察变化
@Observed装饰的类,如果其属性为非简单类型,比如class、Object或者数组,也需要被@Observed装饰,否则将观察不到其属性的变化。
class Child {
public num: number;
constructor(num: number) {
this.num = num;
}
}
@Observed
class Parent {
public child: Child;
public count: number;
constructor(child: Child, count: number) {
this.child = child;
this.count = count;
}
}
以上示例中,Parent被@Observed装饰,其成员变量的赋值的变化是可以被观察到的,但对于Child,没有被@Observed装饰,其属性的修改不能被观察到。
@ObjectLink parent: Parent
// 赋值变化可以被观察到
this.parent.child = new Child(5)
this.parent.count = 5
// Child没有被@Observed装饰,其属性的变化观察不到
this.parent.child.num = 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 NewDate {
public data: DateClass;
constructor(data: DateClass) {
this.data = data;
}
}
@Component
struct Child {
label: string = 'date';
@ObjectLink data: DateClass;
build() {
Column() {
Button(`child increase the day by 1`)
.onClick(() => {
this.data.setDate(this.data.getDate() + 1);
})
DatePicker({
start: new Date('1970-1-1'),
end: new Date('2100-1-1'),
selected: this.data
})
}
}
}
@Entry
@Component
struct Parent {
@State newData: NewDate = new NewDate(new DateClass('2023-1-1'));
build() {
Column() {
Child({ label: 'date', data: this.newData.data })
Button(`parent update the new date`)
.onClick(() => {
this.newData.data = new DateClass('2023-07-07');
})
Button(`ViewB: this.newData = new NewDate(new DateClass('2023-08-20'))`)
.onClick(() => {
this.newData = new NewDate(new DateClass('2023-08-20'));
})
}
}
}
继承Map的class时,可以观察到Map整体的赋值,同时可通过调用Map的接口set, clear, delete 更新Map的值。
继承Set的class时,可以观察到Set整体的赋值,同时可通过调用Set的接口add, clear, delete 更新Set的值。详
限制条件
- 使用@Observed装饰class会改变class原始的原型链,@Observed和其他类装饰器装饰同一个class可能会带来问题。
- @ObjectLink装饰器不能在@Entry装饰的自定义组件中使用。
- @ObjectLink装饰的变量类型需要为显式的被@Observed装饰的类,如果未指定类型,或其不是@Observed装饰的class,编译期会报错。
@Observed
class Info {
count: number;
constructor(count: number) {
this.count = count;
}
}
class Test {
msg: number;
constructor(msg: number) {
this.msg = msg;
}
}
// 错误写法,编译报错
@ObjectLink count;
@ObjectLink test: Test;
// 正确写法
@ObjectLink count: Info;
- @ObjectLink装饰的变量不能本地初始化,仅能通过构造参数从父组件传入初始值,否则编译期会报错。
@Observed
class Info {
count: number;
constructor(count: number) {
this.count = count;
}
}
// 错误写法,编译报错
@ObjectLink count: Info = new Info(10);
// 正确写法
@ObjectLink count: Info;
- @ObjectLink装饰的变量是只读的,不能被赋值,否则会有运行时报错提示Cannot set property when setter is undefined。如果需要对@ObjectLink装饰的变量进行整体替换,可以在父组件对其进行整体替换。
【反例】
@Observed
class Info {
count: number;
constructor(count: number) {
this.count = count;
}
}
@Component
struct Child {
@ObjectLink num: Info;
build() {
Column() {
Text(`num的值: ${this.num.count}`)
.onClick(() => {
// 错误写法,\@ObjectLink装饰的变量不能被赋值
this.num = new Info(10);
})
}
}
}
@Entry
@Component
struct Parent {
@State num: Info = new Info(10);
build() {
Column() {
Text(`count的值: ${this.num}`)
Child({num: this.num})
}
}
}
【正例】
@Observed
class Info {
count: number;
constructor(count: number) {
this.count = count;
}
}
@Component
struct Child {
@ObjectLink num: Info;
build() {
Column() {
Text(`num的值: ${this.num.count}`)
.onClick(() => {
// 正确写法,可以更改@ObjectLink装饰变量的成员属性
this.num.count = 20;
})
}
}
}
@Entry
@Component
struct Parent {
@State num: Info = new Info(10);
build() {
Column() {
Text(`count的值: ${this.num}`)
Button('click')
.onClick(() => {
// 可以在父组件做整体替换
this.num = new Info(30);
})
Child({num: this.num})
}
}
}
fo;
build() {
Column() {
Text(`num的值: ${this.num.count}`)
.onClick(() => {
// 正确写法,可以更改@ObjectLink装饰变量的成员属性
this.num.count = 20;
})
}
}
}
@Entry
@Component
struct Parent {
@State num: Info = new Info(10);
build() {
Column() {
Text(`count的值: ${this.num}`)
Button('click')
.onClick(() => {
// 可以在父组件做整体替换
this.num = new Info(30);
})
Child({num: this.num})
}
}
}