@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
@Prop装饰的变量和父组件建立单向的同步关系:
- @Prop变量允许在本地修改,但修改后的变化不会同步回父组件。
- 当数据源更改时,@Prop装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。
限制条件
- @Prop装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。
- @Prop装饰器不能在@Entry装饰的自定义组件中使用。
嵌套传递层数
在组件复用场景,建议@Prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及GarbageCollection(垃圾回收),引起性能问题,此时更建议使用@ObjectLink
框架行为
要理解@Prop变量值初始化和更新机制,有必要了解父组件和拥有@Prop变量的子组件初始渲染和更新流程。
-
初始渲染:
- 执行父组件的build()函数将创建子组件的新实例,将数据源传递给子组件;
- 初始化子组件@Prop装饰的变量。
-
更新:
- 子组件@Prop更新时,更新仅停留在当前子组件,不会同步回父组件;
- 当父组件的数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地的修改将被父组件的更新覆盖。
使用场景
父组件@State到子组件@Prop简单数据类型同步
@Component
struct CountDownComponent {
@Prop count: number = 0;
costOfOneAttempt: number = 1;
build() {
Column() {
if (this.count > 0) {
Text(`You have ${this.count} Nuggets left`)
} else {
Text('Game over!')
}
// @Prop装饰的变量不会同步给父组件
Button(`Try again`).onClick(() => {
this.count -= this.costOfOneAttempt;
})
}
}
}
@Entry
@Component
struct ParentComponent {
@State countDownStartValue: number = 10;
build() {
Column() {
Text(`Grant ${this.countDownStartValue} nuggets to play.`)
// 父组件的数据源的修改会同步给子组件
Button(`+1 - Nuggets in New Game`).onClick(() => {
this.countDownStartValue += 1;
})
// 父组件的修改会同步给子组件
Button(`-1 - Nuggets in New Game`).onClick(() => {
this.countDownStartValue -= 1;
})
CountDownComponent({ count: this.countDownStartValue, costOfOneAttempt: 2 })
}
}
}
解析:
- 父组件ParentComponent定义State状态变量countDownStartValue,子组件CountDownComponent定义Prop状态变量count接收countDownStartValue值
- 父组件通过按钮的点击改变状态变量countDownStartValue的值,当状态变量值改变,会自动更新相关联的组件,父组件的countDownStartValue值会覆盖子组件count的初始值
- 子组件点击Try again按钮,count值进行改变,在该子组件中改变,不会影响到父组件的countDownStartValue值
父组件@State数组项到子组件@Prop简单数据类型同步
@Component
struct Child {
@Prop value: number = 0;
build() {
Text(`${this.value}`)
.fontSize(50)
.onClick(() => {
this.value++
})
}
}
@Entry
@Component
struct Index {
@State arr: number[] = [1, 2, 3];
build() {
Row() {
Column() {
Child({ value: this.arr[0] })
Child({ value: this.arr[1] })
Child({ value: this.arr[2] })
Divider().height(5)
ForEach(this.arr,
(item: number) => {
Child({ value: item })
},
(item: string) => item.toString()
)
Text('replace entire arr')
.fontSize(50)
.onClick(() => {
// 两个数组都包含项“3”。
this.arr = this.arr[0] == 1 ? [3, 4, 5] : [1, 2, 3];
})
}
}
}
}
解析:
- 父组件Index定义一个State状态变量arr,将arr每项的值分别传递给Child组件,通过单个渲染和循环渲染的方式渲染Child组件,初始为1 2 3------1 2 3
- 点击子组件,将每一位值变为数字7,结果变更为7 7 7-----7 7 7
- 此时子组件的每一位变为了7,但是子组件的value定义为了Prop状态变量,所以父组件的arr值仍然是[1,2,3]
- 当我们点击了replace entire arr文本时,这是数组应该变为[3,4,5],由于forEach存在diff更新的情况,因为原数组[1,2,3]和变化为的数组[3,4,5]都包含元素3,由此将不会对变化为的数组元素为3的位置进行变更修改,又因为元素3是Prop状态变量,被累加到了7,后两位元素3和4,覆盖掉原本Prop状态变量的值,所以最后的结果变成了3 4 5------7 4 5
从父组件中的@State类对象属性到@Prop简单类型的同步
如果图书馆有一本图书和两位用户,每位用户都可以将图书标记为已读,此标记行为不会影响其它读者用户。
class Book {
public title: string;
public pages: number;
public readIt: boolean = false;
constructor(title: string, pages: number) {
this.title = title;
this.pages = pages;
}
}
@Component
struct ReaderComp {
@Prop book: Book = new Book("", 0);
build() {
Row() {
Text(this.book.title)
Text(`...has${this.book.pages} pages!`)
Text(`...${this.book.readIt ? "I have read" : 'I have not read it'}`)
.onClick(() => this.book.readIt = true)
}
}
}
@Entry
@Component
struct Library {
@State book: Book = new Book('100 secrets of C++', 765);
build() {
Column() {
ReaderComp({ book: this.book })
ReaderComp({ book: this.book })
}
}
}
从父组件中的@State数组项到@Prop class类型的同步
@State装饰器只能观察到第一层属性,不会观察到此属性更改,所以框架不会更新ReaderComp。
需要使用@Observed装饰class Book,Book的属性将被观察。
@Observed装饰的类的实例会被不透明的代理对象包装,此代理可以检测到包装对象内的所有属性更改。如果发生这种情况,此时,代理通知@Prop,@Prop对象值被更新。
let nextId: number = 1;
@Observed
class Book {
public id: number;
public title: string;
public pages: number;
public readIt: boolean = false;
constructor(title: string, pages: number) {
this.id = nextId++;
this.title = title;
this.pages = pages;
}
}
@Component
struct ReaderComp {
@Prop book: Book = new Book("", 1);
build() {
Row() {
Text(` ${this.book ? this.book.title : "Book is undefined"}`).fontColor('#e6000000')
Text(` has ${this.book ? this.book.pages : "Book is undefined"} pages!`).fontColor('#e6000000')
Text(` ${this.book ? this.book.readIt ? "I have read" : 'I have not read it' : "Book is undefined"}`).fontColor('#e6000000')
.onClick(() => this.book.readIt = true)
}
}
}
@Entry
@Component
struct Library {
@State allBooks: Book[] = [new Book("C#", 765), new Book("JS", 652), new Book("TS", 765)];
build() {
Column() {
Text('library`s all time favorite')
.width(312)
.height(40)
.backgroundColor('#0d000000')
.borderRadius(20)
.margin(12)
.padding({ left: 20 })
.fontColor('#e6000000')
ReaderComp({ book: this.allBooks[2] })
.backgroundColor('#0d000000')
.width(312)
.height(40)
.padding({ left: 20, top: 10 })
.borderRadius(20)
.colorBlend('#e6000000')
Divider()
Text('Books on loan to a reader')
.width(312)
.height(40)
.backgroundColor('#0d000000')
.borderRadius(20)
.margin(12)
.padding({ left: 20 })
.fontColor('#e6000000')
ForEach(this.allBooks, (book: Book) => {
ReaderComp({ book: book })
.margin(12)
.width(312)
.height(40)
.padding({ left: 20, top: 10 })
.backgroundColor('#0d000000')
.borderRadius(20)
},
(book: Book) => book.id.toString())
Button('Add new')
.width(312)
.height(40)
.margin(12)
.fontColor('#FFFFFF 90%')
.onClick(() => {
this.allBooks.push(new Book("JA", 512));
})
Button('Remove first book')
.width(312)
.height(40)
.margin(12)
.fontColor('#FFFFFF 90%')
.onClick(() => {
if (this.allBooks.length > 0){
this.allBooks.shift();
} else {
console.log("length <= 0")
}
})
Button("Mark read for everyone")
.width(312)
.height(40)
.margin(12)
.fontColor('#FFFFFF 90%')
.onClick(() => {
this.allBooks.forEach((book) => book.readIt = true)
})
}
}
}
@Prop嵌套场景
在嵌套场景下,每一层都要用@Observed装饰,且每一层都要被@Prop接收,这样才能观察到嵌套场景。
// 以下是嵌套类对象的数据结构。
@Observed
class ClassA {
public title: string;
constructor(title: string) {
this.title = title;
}
}
@Observed
class ClassB {
public name: string;
public a: ClassA;
constructor(name: string, a: ClassA) {
this.name = name;
this.a = a;
}
}