写点什么

OpenHarmony 父子组件单项同步使用:@Prop 装饰器

  • 2023-10-08
    北京
  • 本文字数:7504 字

    阅读完需:约 25 分钟

OpenHarmony父子组件单项同步使用:@Prop装饰器

@Prop 装饰的变量可以和父组件建立单向的同步关系。@Prop 装饰的变量是可变的,但是变化不会同步回其父组件。

说明:

从 API version 9 开始,该装饰器支持在 ArkTS 卡片中使用。

概述

@Prop 装饰的变量和父组件建立单向的同步关系:

● @Prop 变量允许在本地修改,但修改后的变化不会同步回父组件。

● 当数据源更改时,@Prop 装饰的变量都会更新,并且会覆盖本地所有更改。因此,数值的同步是父组件到子组件(所属组件),子组件数值的变化不会同步到父组件。

装饰器使用规则说明



变量的传递/访问规则说明

图 1 初始化规则图示  



观察变化和行为表现

观察变化

@Prop 装饰的数据可以观察到以下变化。

● 当装饰的类型是允许的类型,即 Object、class、string、number、boolean、enum 类型都可以观察到赋值的变化。


// 简单类型@Prop count: number;// 赋值的变化可以被观察到this.count = 1;// 复杂类型@Prop count: Model;// 可以观察到赋值的变化this.title = new Model('Hi');
复制代码


当装饰的类型是 Object 或者 class 复杂类型时,可以观察到第一层的属性的变化,属性即 Object.keys(observedObject)返回的所有属性;

class ClassA {  public value: string;  constructor(value: string) {    this.value = value;  }}class Model {  public value: string;  public a: ClassA;  constructor(value: string, a: ClassA) {    this.value = value;    this.a = a;  }}
@Prop title: Model;// 可以观察到第一层的变化this.title.value = 'Hi'// 观察不到第二层的变化this.title.a.value = 'ArkUi'
复制代码

对于嵌套场景,如果 class 是被 @Observed 装饰的,可以观察到 class 属性的变化,示例请参考@Prop嵌套场景

当装饰的类型是数组的时候,可以观察到数组本身的赋值、添加、删除和更新。


// @State装饰的对象为数组时@Prop title: string[]// 数组自身的赋值可以观察到this.title = ['1']// 数组项的赋值可以观察到this.title[0] = '2'// 删除数组项可以观察到this.title.pop()// 新增数组项可以观察到this.title.push('3')
复制代码


对于 @State 和 @Prop 的同步场景:

● 使用父组件中 @State 变量的值初始化子组件中的 @Prop 变量。当 @State 变量变化时,该变量值也会同步更新至 @Prop 变量。

● @Prop 装饰的变量的修改不会影响其数据源 @State 装饰变量的值。

● 除了 @State,数据源也可以用 @Link 或 @Prop 装饰,对 @Prop 的同步机制是相同的。

● 数据源和 @Prop 变量的类型需要相同,@Prop 允许简单类型和 class 类型。

● 当装饰的对象是 Date 时,可以观察到 Date 整体的赋值,同时可通过调用 Date 的接口 setFullYearsetMonthsetDatesetHourssetMinutessetSecondssetMillisecondssetTimesetUTCFullYearsetUTCMonthsetUTCDatesetUTCHourssetUTCMinutessetUTCSecondssetUTCMilliseconds 更新 Date 的属性。

@Componentstruct DateComponent {  @Prop selectedDate: Date = new Date('');
build() { Column() { Button('child update the new date') .margin(10) .onClick(() => { this.selectedDate = new Date('2023-09-09') }) Button(`child increase the year by 1`).onClick(() => { this.selectedDate.setFullYear(this.selectedDate.getFullYear() + 1) }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.selectedDate }) } }}
@Entry@Componentstruct ParentComponent { @State parentSelectedDate: Date = new Date('2021-08-08');
build() { Column() { Button('parent update the new date') .margin(10) .onClick(() => { this.parentSelectedDate = new Date('2023-07-07') }) Button('parent increase the day by 1') .margin(10) .onClick(() => { this.parentSelectedDate.setDate(this.parentSelectedDate.getDate() + 1) }) DatePicker({ start: new Date('1970-1-1'), end: new Date('2100-1-1'), selected: this.parentSelectedDate })
DateComponent({selectedDate:this.parentSelectedDate}) }
}}
复制代码

框架行为

要理解 @Prop 变量值初始化和更新机制,有必要了解父组件和拥有 @Prop 变量的子组件初始渲染和更新流程。

1.  初始渲染:

a.  执行父组件的 build()函数将创建子组件的新实例,将数据源传递给子组件;

b.  初始化子组件 @Prop 装饰的变量。

2.  更新:

a.  子组件 @Prop 更新时,更新仅停留在当前子组件,不会同步回父组件;

b.  当父组件的数据源更新时,子组件的 @Prop 装饰的变量将被来自父组件的数据源重置,所有 @Prop 装饰的本地的修改将被父组件的更新覆盖。

使用场景

父组件 @State 到子组件 @Prop 简单数据类型同步

以下示例是 @State 到子组件 @Prop 简单数据同步,父组件 ParentComponent 的状态变量 countDownStartValue 初始化子组件 CountDownComponent 中 @Prop 装饰的 count,点击“Try again”,count 的修改仅保留在 CountDownComponent 不会同步给父组件 ParentComponent。

ParentComponent 的状态变量 countDownStartValue 的变化将重置 CountDownComponent 的 count。

@Componentstruct 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@Componentstruct 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 }) } }}
复制代码

在上面的示例中:

1.  CountDownComponent 子组件首次创建时其 @Prop 装饰的 count 变量将从父组件 @State 装饰的 countDownStartValue 变量初始化;

2.  按“+1”或“-1”按钮时,父组件的 @State 装饰的 countDownStartValue 值会变化,这将触发父组件重新渲染,在父组件重新渲染过程中会刷新使用 countDownStartValue 状态变量的 UI 组件并单向同步更新 CountDownComponent 子组件中的 count 值;

3.  更新 count 状态变量值也会触发 CountDownComponent 的重新渲染,在重新渲染过程中,评估使用 count 状态变量的 if 语句条件(this.count > 0),并执行 true 分支中的使用 count 状态变量的 UI 组件相关描述来更新 Text 组件的 UI 显示;

4.  当按下子组件 CountDownComponent 的“Try again”按钮时,其 @Prop 变量 count 将被更改,但是 count 值的更改不会影响父组件的 countDownStartValue 值;

5.  父组件的 countDownStartValue 值会变化时,父组件的修改将覆盖掉子组件 CountDownComponent 中 count 本地的修改。

父组件 @State 数组项到子组件 @Prop 简单数据类型同步

父组件中 @State 如果装饰的数组,其数组项也可以初始化 @Prop。以下示例中父组件 Index 中 @State 装饰的数组 arr,将其数组项初始化子组件 Child 中 @Prop 装饰的 value。

@Componentstruct Child {  @Prop value: number = 0;
build() { Text(`${this.value}`) .fontSize(50) .onClick(()=>{this.value++}) }}
@Entry@Componentstruct 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: void) => { 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]; }) } } }}
复制代码

初始渲染创建 6 个子组件实例,每个 @Prop 装饰的变量初始化都在本地拷贝了一份数组项。子组件 onclick 事件处理程序会更改局部变量值。

假设我们点击了多次,所有变量的本地取值都是“7”。


777----777
复制代码


单击 replace entire arr 后,屏幕将显示以下信息。


345----745
复制代码


● 在子组件 Child 中做的所有的修改都不会同步回父组件 Index 组件,所以即使 6 个组件显示都为 7,但在父组件 Index 中,this.arr 保存的值依旧是[1,2,3]。

● 点击 replace entire arr,this.arr[0] == 1 成立,将 this.arr 赋值为[3, 4, 5];

● 因为 this.arr[0]已更改,Child({value: this.arr[0]})组件将 this.arr[0]更新同步到实例 @Prop 装饰的变量。Child({value: this.arr[1]})和 Child({value: this.arr[2]})的情况也类似。

● this.arr 的更改触发 ForEach 更新,this.arr 更新的前后都有数值为 3 的数组项:[3, 4, 5] 和[1, 2, 3]。根据 diff 机制,数组项“3”将被保留,删除“1”和“2”的数组项,添加为“4”和“5”的数组项。这就意味着,数组项“3”的组件不会重新生成,而是将其移动到第一位。所以“3”对应的组件不会更新,此时“3”对应的组件数值为“7”,ForEach 最终的渲染结果是“7”,“4”,“5”。

从父组件中的 @State 类对象属性到 @Prop 简单类型的同步

如果图书馆有一本图书和两位用户,每位用户都可以将图书标记为已读,此标记行为不会影响其它读者用户。从代码角度讲,对 @Prop 图书对象的本地更改不会同步给图书馆组件中的 @State 图书对象。

在此示例中,图书类可以使用 @Observed 装饰器,但不是必须的,只有在嵌套结构时需要此装饰器。这一点我们会在从父组件中的@State数组项到@Prop class类型的同步说明。

class Book {  public title: string;  public pages: number;  public readIt: boolean = false;
constructor(title: string, pages: number) { this.title = title; this.pages = pages; }}
@Componentstruct 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@Componentstruct 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 修饰的 allBooks 数组中 Book 对象上的属性,但点击“Mark read for everyone”无反应。这是因为该属性是第二层的嵌套属性,@State 装饰器只能观察到第一层属性,不会观察到此属性更改,所以框架不会更新 ReaderComp。


let nextId: number = 1;
// @Observedclass 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; }}
@Componentstruct ReaderComp { @Prop book: Book = new Book("", 1);
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@Componentstruct Library { @State allBooks: Book[] = [new Book("100 secrets of C++", 765), new Book("Effective C++", 651), new Book("The C++ programming language", 1765)];
build() { Column() { Text('library`s all time favorite') ReaderComp({ book: this.allBooks[2] }) Divider() Text('Books on loaan to a reader') ForEach(this.allBooks, (book: Book) => { ReaderComp({ book: book }) }, (book: Book) => book.id.toString()) Button('Add new') .onClick(() => { this.allBooks.push(new Book("The C++ Standard Library", 512)); }) Button('Remove first book') .onClick(() => { this.allBooks.shift(); }) Button("Mark read for everyone") .onClick(() => { this.allBooks.forEach((book) => book.readIt = true) }) } }}
复制代码


需要使用 @Observed 装饰 class Book,Book 的属性将被观察。 需要注意的是,@Prop 在子组件装饰的状态变量和父组件的数据源是单向同步关系,即 ReaderComp 中的 @Prop book 的修改不会同步给父组件 Library。而父组件只会在数值有更新的时候(和上一次状态的对比),才会触发 UI 的重新渲染。

@Observedclass 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; }}
复制代码

@Observed 装饰的类的实例会被不透明的代理对象包装,此代理可以检测到包装对象内的所有属性更改。如果发生这种情况,此时,代理通知 @Prop,@Prop 对象值被更新。

@Prop 本地初始化不和父组件同步

为了支持 @Component 装饰的组件复用场景,@Prop 支持本地初始化,这样可以让 @Prop 是否与父组件建立同步关系变得可选。当且仅当 @Prop 有本地初始化时,从父组件向子组件传递 @Prop 的数据源才是可选的。

下面的示例中,子组件包含两个 @Prop 变量:

● @Prop customCounter 没有本地初始化,所以需要父组件提供数据源去初始化 @Prop,并当父组件的数据源变化时,@Prop 也将被更新;

● @Prop customCounter2 有本地初始化,在这种情况下,@Prop 依旧允许但非强制父组件同步数据源给 @Prop。

@Componentstruct MyComponent {  @Prop customCounter: number = 0;  @Prop customCounter2: number = 5;
build() { Column() { Row() { Text(`From Main: ${this.customCounter}`).width(90).height(40).fontColor('#FF0010') }
Row() { Button('Click to change locally !').width(180).height(60).margin({ top: 10 }) .onClick(() => { this.customCounter2++ }) }.height(100).width(180)
Row() { Text(`Custom Local: ${this.customCounter2}`).width(90).height(40).fontColor('#FF0010') } } }}
@Entry@Componentstruct MainProgram { @State mainCounter: number = 10;
build() { Column() { Row() { Column() { Button('Click to change number').width(480).height(60).margin({ top: 10, bottom: 10 }) .onClick(() => { this.mainCounter++ }) } }
Row() { Column() { // customCounter必须从父组件初始化,因为MyComponent的customCounter成员变量缺少本地初始化;此处,customCounter2可以不做初始化。 MyComponent({ customCounter: this.mainCounter }) // customCounter2也可以从父组件初始化,父组件初始化的值会覆盖子组件customCounter2的本地初始化的值 MyComponent({ customCounter: this.mainCounter, customCounter2: this.mainCounter }) } } } }}
复制代码

@Prop 嵌套场景

在嵌套场景下,每一层都要用 @Observed 装饰,且每一层都要被 @Prop 接收,这样才能观察到嵌套场景。

// 以下是嵌套类对象的数据结构。@Observedclass ClassA {  public title: string;
constructor(title: string) { this.title = title; }}
@Observedclass ClassB { public name: string; public a: ClassA;
constructor(name: string, a: ClassA) { this.name = name; this.a = a; }}
复制代码

以下组件层次结构呈现的是 @Prop 嵌套场景的数据结构。

@Entry@Componentstruct Parent {  @State votes: ClassB = new ClassB('Hello', new ClassA('world'))
build() { Column() { Button('change') .onClick(() => { this.votes.name = "aaaaa" this.votes.a.title = "wwwww" }) Child({ vote: this.votes }) }
}}
@Componentstruct Child { @Prop vote: ClassB = new ClassB('', new ClassA('')); build() { Column() {
Text(this.vote.name).fontSize(36).fontColor(Color.Red).margin(50) .onClick(() => { this.vote.name = 'Bye' }) Text(this.vote.a.title).fontSize(36).fontColor(Color.Blue) .onClick(() => { this.vote.a.title = "openHarmony" }) Child1({vote1:this.vote.a})
} }}
@Componentstruct Child1 { @Prop vote1: ClassA = new ClassA(''); build() { Column() { Text(this.vote1.title).fontSize(36).fontColor(Color.Red).margin(50) .onClick(() => { this.vote1.title = 'Bye Bye' }) } }}
复制代码


用户头像

OpenHarmony开发者官方账号 2021-12-15 加入

OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展

评论

发布
暂无评论
OpenHarmony父子组件单项同步使用:@Prop装饰器_OpenHarmony开发者_InfoQ写作社区