科技 > 操作系统 > 鸿蒙系统

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

143人参与 2024-08-06 鸿蒙系统

【中秋国庆不断更】openharmony父子组件单项同步使用:@prop装饰器

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

说明:

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

概述

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

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

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

装饰器使用规则说明

@prop变量装饰器 说明
装饰器参数
同步类型 单向同步:对父组件状态变量值的修改,将同步给子组件@prop装饰的变量,子组件@prop变量的修改不会同步到父组件的状态变量上。嵌套类型的场景请参考观察变化
允许装饰的变量类型 object、class、string、number、boolean、enum类型,以及这些类型的数组。不支持any,不支持简单类型和复杂类型的联合类型,不允许使用undefined和null。支持date类型。支持类型的场景请参考观察变化。必须指定类型。说明 :不支持length、resourcestr、resourcecolor类型,length,resourcestr、resourcecolor为简单类型和复杂类型的联合类型。在父组件中,传递给@prop装饰的值不能为undefined或者null,反例如下所示。compa ({ aprop: undefined })compa ({ aprop: null })@prop和数据源类型需要相同,有以下三种情况:- @prop装饰的变量和@state以及其他装饰器同步时双方的类型必须相同,示例请参考父组件@state到子组件@prop简单数据类型同步。- @prop装饰的变量和@state以及其他装饰器装饰的数组的项同步时 ,@prop的类型需要和@state装饰的数组的数组项相同,比如@prop : t和@state : array<t>,示例请参考父组件@state数组中的项到子组件@prop简单数据类型同步;- 当父组件状态变量为object或者class时,@prop装饰的变量和父组件状态变量的属性类型相同,示例请参考从父组件中的@state类对象属性到@prop简单类型的同步
嵌套传递层数 在组件复用场景,建议@prop深度嵌套数据不要超过5层,嵌套太多会导致深拷贝占用的空间过大以及garbagecollection(垃圾回收),引起性能问题,此时更建议使用@objectlink。如果子组件的数据不想同步回父组件,建议采用@reusable中的abouttoreuse,实现父组件向子组件传递数据,具体用例请参考组件复用场景
被装饰变量的初始值 允许本地初始化。

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

传递/访问 说明
从父组件初始化 如果本地有初始化,则是可选的。没有的话,则必选,支持父组件中的常规变量(常规变量对@prop赋值,只是数值的初始化,常规变量的变化不会触发ui刷新。只有状态变量才能触发ui刷新)、@state、@link、@prop、@provide、@consume、@objectlink、@storagelink、@storageprop、@localstoragelink和@localstorageprop去初始化子组件中的@prop变量。
用于初始化子组件 @prop支持去初始化子组件中的常规变量、@state、@link、@prop、@provide。
是否支持组件外访问 @prop装饰的变量是私有的,只能在组件内访问。

图1 初始化规则图示

img

观察变化和行为表现

观察变化

@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的接口setfullyear, setmonth, setdate, sethours, setminutes, setseconds, setmilliseconds, settime, setutcfullyear, setutcmonth, setutcdate, setutchours, setutcminutes, setutcseconds, setutcmilliseconds 更新date的属性。

@component
struct 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
@component
struct 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。

@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 })
    }
  }
}

在上面的示例中:

​ 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。

@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: 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”。

7
7
7
----
7
7
7

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

3
4
5
----
7
4
5

​ ● 在子组件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;
  }
}

@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 修饰的allbooks数组中book对象上的属性,但点击“mark read for everyone”无反应。这是因为该属性是第二层的嵌套属性,@state装饰器只能观察到第一层属性,不会观察到此属性更改,所以框架不会更新readercomp。

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.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 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的重新渲染。

@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;
  }
}

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

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

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

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

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

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

@component
struct 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
@component
struct 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接收,这样才能观察到嵌套场景。

// 以下是嵌套类对象的数据结构。
@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;
  }
}

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

@entry
@component
struct 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 })
    }

  }
}

@component
struct 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})

    }
  }
}

@component
struct 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'
        })
    }
  }
}

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

深入理解HarmonyOS UIAbility:生命周期、WindowStage与启动模式探析

08-06

鸿蒙开发丨设备内UIAbility的几种交互方式

08-06

从应用迁移到平台微认证:鲲鹏技术解读

08-06

华为鲲鹏Kworker进程占用CPU100解决方案

08-06

华为鸿蒙 HarmonyOS NEXT Developer Beta3 更新:新增 WLAN 和移动网络多通道收发数据、小艺输入法自动纠错等

08-05

鸿蒙生态进入第二阶段,加速千行百业应用鸿蒙化

08-04

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论