HarmonyOS状态管理之组件通讯

状态管理(组件通讯)

1:概述

  1. 在使用状态管理之前,我们所构建的页面大多数为静态页面,如果希望构建一个动态的,有交互的界面,就要引用‘状态’的概念

  2. 状态的概念:

    • 在ArkUI框架中,UI是程序运行的结果,用户构建了一个UI模型,其中运行时的状态为参数,当参数改变时,UI作为返回的结果,也会随之发生了改变,这些运行的变化所带来的UI重新渲染,在ArkUI中统称为状态管理机制(什么是状态管理机制)

    • 自定义组件拥有变量(成员变量),变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起UI的渲染的刷新,如果不使用状态变量,UI只能在初始状态时渲染,后续不在刷新

    •   /* UI(View):UI渲染,将build方法内的UI描述和@Builder装饰的方法内的UI描述映射到界面
         * State:状态,驱动UI更新的数据,用户通过触发组件的事件方法,改变状态数据,状态数据的改变引起UI重新渲染
         * render:渲染
         * event-handlers:事件函数
         */
      

2:基本概念

  1. 状态变量:被状态装饰器所装饰的变量,状态变量的改变会引起UI渲染的更新
  2. 常规变量:没有被状态装饰器所装饰的变量,通常应用于辅助计算,通常其改变不会引起UI的刷新
  3. 数据源/同步源状态变量的初始来源,可以同步给不同的状态数据,通常意义父组件传给子组件的数据
  4. 命名参数机制:父组件通过指定参数传给子组件的状态变量,为父子传递同步参数的主要手段 son({num:this.count})
  5. 从父组件初始化:父组件使用命名参数机制,将指定参数传给子组件。子组件初始化的默认值在有父组件传值的情况下,会被覆盖

3:装饰器总览

  1. ArkUI状态管理V1,提供了多种装饰器,通过这种装饰器,状态变量不仅可以观察在组件内改变(组件通讯),也可以观察全局范围内的变化(应用通讯
  2. 可以在不同组件层级间传递,比如父子组件,跨组件层级,根据状态变量影响的范围将所有的装饰器大致分为两类:
    • 管理组件拥有的状态:组件级别的状态管理,可以观察组件内的变化,和不同组件层级的变化,但需要唯一观察同意组件树上,即同一页面
    • 管理应用拥有的状态:应用级别的状态管理,可以观察不同页面,甚至UIAbility的状态变化是应用全局的状态管理 从数据传递形式和同步类型层面,装饰分为:只读的单向传递,可变更的双向传递

4:管理组件拥有的状态

  1. @State:@State装饰的变量拥有其所属组的状态,可以作为其子组件单项和双向同步的数据源
  2. @Prop:@Prop装饰的变量可以和父组件建立单项同步关系,但修改不会同步父组件
  3. @Link:@Link装饰器的变量和父组件构建双向同步关系,父组件会接受来自@Link装饰的变量的修改的同步,父组件的更新也会同步给@Link装饰的变量
  4. @Provide/@Consume:@Provide/@Consume装饰的变量用于跨组件通讯(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过alias(别名)或者属性名绑定
  5. @Observed:@Observed装饰class,需要观察多层嵌套场景的class,需要被Observed所装饰。单独使用@Observed没有任何作用需要和@ObjectLink,@Prop连用
  6. @ObjectLink:@ObjectLink装饰的变量接收@Observed装饰的class实例应用与观察多层嵌套场景,和父组件的数据源构建为双向同步
  7. 其他状态管理功能:
    • &&运算符:给内置组件提供ts变量的引用,使得ts变量和内置组件的内部状态保持同步
    • @Watch:用于监听这状态变量的变化

5:@State

5.1:概念

  1. 在状态变量相关装饰器中,@State是最基础的,使变量拥有状态属性的装饰器,他也是大部分状态变量的数据源
  2. @State所装饰的变量是私有的,只能从组件内部访问,在声明时必须指定类型和本地初始化,初始化也可以选择使用命名参数机制(从父组件完成初始化(初始化覆盖))

5.2:@State装饰的变量的特点

  1. @State装饰的变量与子组件中的@Prop装饰的变量之间建立单向数据同步与@Link,@ObjectLink装饰的变量之间形成双向同步

  2. @State装饰的变量的生命周期与其所属自定义组件的生命周期相同

  3.  /*    使用规则:
          *      同步类型:不与父组件任何的类型变量同步
          *      允许装饰的变量类型:
          *          基本类似:number,boolean,string
          *          引用类似:object,class,enum(枚举)
          *          以及这些类型的数组
          *        AIP11以后
          *          允许装饰Set Map
          *          允许支持装饰undefined和null
          *          允许支持装饰联合类型
          *            比如:string | number
          *      被装饰的变量的初始值:必须本地初始化
          */
    

5.3:变量的传递/访问

  • 从父组件初始化:可选,可以从父组件初始化或本地初始化,从父组件初始化将会覆盖本地初始化

    • 支持父组件中常规变量:@State,@Link,@Prop,@Provide,@Consume,@ObjectLink
  • 用于初始化子组件:

    • @State装饰的变量支持初始化子组件常规变量:@State,@Link,@Prop,@Provide

    • 不支持外部组件访问,只能在组件内部访问

5.4:观察变化和行为

  1. 并不是状态变量的所有更改都会引起UI刷新,只有可以被框架观察到的修改才会引起UI刷新

  2. 当装饰的数据类型为基本类型(string,number,boolean)可以观察到数值变化

  3. 当装饰的数据为class或者Object,可以观察到自身的赋值变化,和其属性的赋值的变化第一层嵌套属性的赋值观察不到第二层

  4. **ps:**嵌套属性的赋值(第二层)如果和非嵌套(第一层)一起使用[UI结果在同一组件里面]也会触发值的改变(非嵌套属性发生改变,那么嵌套的组件属性也发生改变前提在同一组件中))

  5. 当装饰的对象是array时,可以观察到数组本身(赋值,添加,删除,更新数组的变化) 数组项的值可以观察到,数组项中的属性变化观察不到

    •   //      * pop 删除最后一个
        //      * shift 删除第一个
        //      * push 向数组最后一个添加
        //      * unshift 向数组第一个添加
      
  6. 当装饰的对象是Date时,可以观察到Date的整体赋值,同时可通过调用一系列的Date接口(setFullYear…)更新Date属性

    •   // import { ClassA } from './Aug_12_am'
        
        @Entry
        @Component
        struct Aug_12_pm {
          // @State message: string = 'Hello World';
          //@State arr:ClassA[] = [new ClassA('a'),new ClassA('b')]
          @State _time: Date = new Date()
          build() {
            Column({space : 20}){
              // Text('Time')
              //   .onClick(() => {
              //     //当前计算机的时间戳
              //     // console.log(`${this._time}`)
              //     //let year = this._time.getFullYear()
              //     //let month = this._time.getMonth() 比正常月份少1
              //     //let week = this._time.getDay() 星期几
              //     //let day = this._time.getDate() 几号
              //     //let _h = this._time.getHours() 时
              //     //let _m = this._time.getMinutes() 分
              //     //let _s = this._time.getSeconds() 秒
              //     // let _mill = this._time.getMilliseconds()
              //     //目标时
              //     // let newDate = new Date("2024/8/12,22:00:00")
              //     // //此为2024/8/12,22:00:00 距离 1970/1/1 00:00:00 的时间差 单位ms
              //     // let endTime = newDate.getTime()
              //     // console.log(`${endTime}`) //1970/1/1 - 2024/8/12 单位:ms
              //     // //此为当前距离 1970/1/1 00:00:00 的时间差 单位ms
              //     // let timer = this._time.getTime()
              //     // //两个值相减即为现在距离晚上10点的时间差
              //     // console.log(`${((endTime - timer)/1000/3600).toFixed(2)}`)
              //     //格林威治时间和本地时间之间的时差,以分钟为单位
              //     // let tomeZone = this._time.getTimezoneOffset()
              //     // console.log(`${tomeZone}`)
              //
              //
              //   })
              //   .fontSize(30)
              //日期选择器组件,用于根据指定日期范围创建日期滑动选择器
              Button('选择 2023年8月12日')
                .onClick(() => {
                  this._time = new Date('2023-08-12')
                })
              Button('选择下一年')
                .onClick(() => {
                  this._time.setFullYear(this._time.getFullYear() + 1)
                })
              Button('选择下一个月')
                .onClick(() => {
                  this._time.setMonth(this._time.getMonth() + 1)
                })
              DatePicker({
                //start 日历开始时间,默认为1970
                start:new Date('1999-03-23'),
                //end 日历结束时间 默认为2100
                end: new Date('2050-01-01'),
                //设置选中项的日期。默认值:当前系统日期
                selected:this._time
              })
                //设置弹窗的日期是否显示农历
                .lunar(false)
                  //事件 选择日期时触发该事件
                .onDateChange((value) => {
                  console.log(`${value}`)
                })
              //时间选择器组件
              // TimePicker()
              //倒计时组件
              //TextTimer()
              
            }
            .width('100%')
            .height('100%')
            .justifyContent(FlexAlign.Center)
          }
        }
      

6:@Prop

6.1:概念

  1. @Prop装饰器:父子单向同步
    • @Prop装饰的变量和父组件之间建立单向同步关系,父组件的改变可以同步到Prop装饰的变量
    • @Prop变量允许在本地修改,但是修改后的变化不会同步回父组件
  2. 限制条件:
    • @Prop装饰器不能在@Entry装饰的自定义组件中使用
    • @prop装饰变量时会进行深拷贝,在拷贝过程当中,如果是复杂类型的数据,会出丢失类型的情况,例如PixelMap等通过NAPI提供的复杂类型。在ArkTS侧无法获取完整的数据
    • 允许装饰变量的类型同@State一致
    • @Prop必须指定类型,和数据源类型必须相同
  3. 同步类型:
    • 单向同步:对父组件状态变量值的修改。将同步给子组件@Prop装饰的变量,子组件Prop变量的修改不会同步到父组件状态变量上

6.2:注意

  1. 在组件复用场景中,@Prop深度嵌套数据不要超过5层,因为@Prop为深拷贝,嵌套太多会导致深拷贝占用空间过大以及垃圾回收引起性能问题
    • 深拷贝:拷贝相同的内容,访问的是源数据的拷贝不是源数据
    • 浅拷贝:获取数据源的地址
  2. 从父组件初始化,如果没有本地初始化,则是必选的,支持父组件中的常规变量(不会引起UI更新),State,Link,Prop,Provide,Consume,ObjectLink去初始化子组件中的@Prop变量(@Prop作为子存在)
  3. 用于初始化子组件(作为父存在)@State,@Link,@Prop,@Provide

6.3:观察变化

  1. 当装饰的类型是允许的类型,object,class,string,number,boolean,enum类型都可以观察到赋值的变化
  2. 当装饰的类型是Object或class,可以观察到第一层变化
  3. 对于嵌套场景,如果class是被@Observed装饰,子组件(@Prop修饰的)可以观察属性变化
  4. 当装饰的类型是数组时,可以观察到数组本身的赋值和数组项的添加和删除
  5. 对于@State与@Prop同步时:
    1. 使用父组件@State变量的值初始化子组件中@Prop变量,@Prop装饰的变量的修改不会影响其数据源@State所装饰的变量, 除了@State,数据源可以使用@Link、@Prop装饰,对于@Prop的同步机制相同,数据源类型需要与@Prop变量类型相同

6.4:框架行为(重)

  1. 初次渲染:
    • 执行父组件的build()函数将创建子组件实例, 将数据源传递给子组件
    • 初始化子组件@Prop装饰的变量
  2. 更新(二次渲染)
    • 子组件@Prop更新时,更新仅停留在当前子组件,不会同步回父组件
    • 当父组件数据源更新时,子组件的@Prop装饰的变量将被来自父组件的数据源重置,所有@Prop装饰的本地修改将父组件更新覆盖

6.5:使用场景

  1. 父组件@State到子组件@Prop简单是数据类型同步
  2. 父组件@State数组项到子组件@Prop简单数据同步 *
  3. 父组件中的@State类对象属性到@Prop简单类型的同步
  4. 父组件中的@State数组项到@Prop class类型的同步*
  5. @Prop的嵌套场景*
6.5.1:父组件@State数组项到子组件@Prop简单数据同步
@Entry
@Component
  /*
    作者:ZhuWei
    时间:2024/8/13
*/
struct mytest03 {
  @State arr:number[] = [1,2,3]
  build() {
    Column({space:20}) {

      //父组件@State数组项到子组件@Prop简单数据同步
      Text(`父组件的number:${JSON.stringify(this.arr)}`)
        .fontSize(30)
      Child({ num:this.arr[0] })
      Child({ num:this.arr[1] })
      Child({ num:this.arr[2] })
      ForEach(this.arr,(item:number) => {
        Child({ num: item })
      },(item:string) => item.toString())
      Text('更新数组项里面的数据')
        .fontSize(30)
        .onClick(() => {
              /*
                  最开始的键值: '1' '2' '3'
               * [1, 2, 3]
               *  生成键值: '3' '4' '5'
                * [3, 4, 5]
               *
               * */
              /*
               * 初始渲染创6个子组件实例,每个@Prop装饰
               * 的变量初始化都在本地拷贝一份数组项。子组件onclick
               * 事件处理程序会更该局部变量
               * 如果点击 界面上的
               *  '1' 3下 '2' 3下 '3' 3下 将变量本地的值依次变为 '456456'
               * 点击 '更新数组项里面的数据'按钮后 依次变为'345645'
               * 在子组件Child中做的所有修改都不会同步会父组件,所以所有子组件不管如何点击
               * 但在父组件Index中的arr始终为[1,2,3]
               * 点击 '更新数组项里面的数据' 按钮后 this.arr[0] == 1 成立
               * this.arr赋值为 3,4,5
               * 因为this.arr更改 child({ num:this.arr[] })组件将this.arr
               * 更新同步到实例@Prop装饰的变量
               * this.arr更改触发ForEach的更新,this.arr更新前后都有数值为3的数组项
               * 根据diff算法,同名键值3会被保留,删除 1、2,添加为 4、5
               * 这就意味着3不会重新生成,而是移动到更新后的第一位,即原先为3的键值渲染的结果移接掉现在键值为3的键值中
               *
               *
               * */
          this.arr =
            this.arr[0] == 1 ?
              [3, 4, 5] : [1, 2, 3]
        })

    }
    .height('100%')
    .width('100%')
  }
}
//父组件@State数组项到子组件@Prop简单数据同步
@Component
struct Child{
  @Prop num:number
  // @Prop title:Model
  build() {
    Column(){
      Text(`子组件的number:${this.num}`)
        .onClick(() => {
          this.num++
        })
        .fontSize(30)
      // Text(`${JSON.stringify(this.title)}`)
      //   .fontSize(30)
    }
  }
}
6.5.2:父组件中的@State数组项到@Prop class类型的同步
let nextId: number = 1
@Observed
class Book{
  id: number
  title: string
  pages: number
  read: boolean = false

  constructor(title: string, pages: number) {
    this.id = nextId++
    this.title = title
    this.pages = pages
  }

}
/*
 * 案例知识点:
 * (1):?:判断的的嵌套
 * (2):@Prop,的变更不会影响到父组件的变更(数据单向传递),而父组件的刷新会引起子组件的重新刷新渲染
 * (3):直接修改第二层的数据(被数组包裹的类里面的属性)是无法被父组件观察到的,需要使用@Observed装饰class Book,Book属性被观察到
 * (4)@Observed装饰的类的实例会被不透明的代理对象包装,此代理可以检测到包装对象内的所有属性更改,如果发生这种情况,代理通知@Prop,@Prop对象的值跟新
 *
 *
 * */


@Entry
@Component
  /*
    作者:ZhuWei
    时间:2024/8/13
*/
struct mytest01 {
  @State message: string = 'Hello World';
  @State allBooks:Book[] = [
    new Book("母猪的产后护理",700),
    new Book("如何获取富婆欢心",800),
    new Book("霸道总裁爱上我",1000)
  ]
  build() {
    Column({space:20}) {
      Text('第一位借阅者')
        .fontSize(30)
        .fontWeight(800)
      // 展示数组中第一本书
      Reader({ book: this.allBooks[0] })
      Text(`父组件的借阅者${JSON.stringify(this.allBooks[0])}`)
        .fontSize(30)

      Text('其他借阅者')
        .fontSize(30)
        .fontWeight(800)

      ForEach(this.allBooks,(item:Book) => {
        Reader({ book:item })
      },(item: Book) => item.id.toString())
      Button('添加一本书')
        .onClick(() => {
          this.allBooks.push(new Book("我的奋斗",1200))
        })
      Button('删除第一本书')
        .onClick(() => {
          if(this.allBooks.length > 0){
            this.allBooks.shift()
          }else{
            console.log('图书馆没书了!!!')
          }
        })

      // 改写第二层数据,需要class类被@Observed修饰才可被渲染监听
      Button("所有书籍都被阅读完毕")
        .onClick(() => {
          this.allBooks.forEach((item:Book) => item.read = true)
        })

    }    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)  }
}

//父组件中的@State数组项到@Prop class类型的同步
@Component
struct Reader{
  @Prop book: Book = new Book('',0)
  build() {
    Row(){
      Text(`${this.book ? this.book.title : '这本书不存在'}`)
        .fontColor(Color.Red)
        .fontSize(20)
      Text(`${this.book ? this.book.pages : '这本书不存在'}`)
        .fontColor(Color.Blue)
        .fontSize(20)
      Text(`${this.book ? this.book.read ? '已经读完了' : '没有读完' : '这本书不存在'}`)
        .fontWeight(600)
        .onClick(() => {
          this.book.read = true
        })
    }
  }
}
6.5.3:@Prop的嵌套场景
//在嵌套场景下,每一层都需要用@Observed装饰,且每一层都要被@Prop接受,这样才能观察嵌套场景
//修改第二层数据,父组件监听不到第二层数据,由于传递的是数据项,相当于子组件监听的是第一层的数据(相对于父组件是第二层)子组件可以遍历渲染
// 当第一层数据刷新时,会带动第二层的数据,所以第二层数据的变化,在父组件渲染出来
@Observed
class ClassA{
  title: string

  constructor(title: string) {
    this.title = title
  }

}
@Observed
class ClassB{
  name:string
  a:ClassA

  constructor(name: string, a: ClassA) {
    this.name = name
    this.a = a
  }

}

@Entry
@Component
  /*
    作者:ZhuWei
    时间:2024/8/13
*/
struct mytest02 {
  @State message: string = 'Hello World';
  @State votes:ClassB = new ClassB('Hello',new ClassA('world'))
  build() {
    Column() {
      //@Prop的嵌套场景
      Button('修改ClassB name')
        .onClick(() => {
          this.votes.name = 'Hi'
        })
      Button('修改ClassA title')
        .onClick(() => {
          this.votes.a.title = 'ArkTs'
        })

      Text(`ClassB name:${this.votes.name}`)
        .fontSize(30)
        .onClick(() => {
          this.votes.name = 'Bye'
        })

      Text(`ClassA title:${this.votes.a.title}`)
        .fontSize(30)
        .onClick(() => {
          this.votes.a.title = 'openHarmony'
        })
      ChildA({ voteA: this.votes.a })

    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

//@Prop嵌套场景
@Component
struct ChildA{
  @Prop voteA: ClassA = new ClassA('')
  build() {
    Column(){
      Text(`子组件ClassA title:${this.voteA.title}`)
        .fontSize(30)
        .onClick(() => {
          this.voteA.title = 'Bye'
        })
    }
  }
}

6.6:@State和@Prop异同

  1. 被@State和@Prop装饰的变量自动成为状态变量,变量的更新触发UI的刷新
  1. @State作为子组件的变量接收父组件参数无法形成同步
  2. @Prop作为子组件变量接收父组件参数可以形成单向同步
  3. @State装饰的变量必须本地初始化而且要指定类型
  4. @Prop装饰的本地变量本地初始化雨从父组件初始化选择其一,必须指定类型

7:@Link

7.1:概念

  1. @Link装饰器:父子双向同步,子组件中被@Link装饰的变量与其父组件对应的数据源建立双向数据绑定
  2. 限制条件:@Link装饰器不能再@Entry装饰的自定义组件中使用,@Link禁止本地初始化
  3. 同步类型:双向同步

7.2:变量的传递与访问

  • 从父组件初始化更新:必选,与@State,@StorageLink,@Link建立双向绑定
  • 允许父组件中……装饰变量初始化子组件@Link
  • 用于初始化子组件:可以初始化常规变量,@State,@Link,@Prop,@Provide不允许组件外访问,只能所属组件访问

7.3:观察变化

  • 当装饰的数据为boolean、string、number类型,可以同步观察数值变化
  • 当装饰的类型为class、object,可以观察赋值变化与属性赋值变化
  • 当装饰的类型为array,数组添加、删除、更新数组单元变化
  • 当装饰的类型为Date,可以观察Date整体的赋值以及一系列接口调用

7.4:框架行为

7.4.1:初始渲染
  • 执行父组件的build( )函数实例化子组件
  1. 必须指定父组件中的@State变量,用于初始化子组件的@Link变量,子组件的@Link变量值与其父组件的数据源变量保持同步(双向同步)
  2. 父组件的@State状态变量包装类(将变量的地址传递给了子组件)通过构造函数传给子组件,子组件的@Link拿到父组件的@State的状态变量包装类后, 将当前@Link包装类this指针注册给父组件的@State变量(父子交换数据地址)
7.4.2:@Link的数据源的更新
  • 即父组件中状态变量更新引起相关子组件的@Link的更新
  1. 通过初步渲染的步骤可知,子组件@Link包装类把当前的this指针注册给父组件(将数据地址给到了父组件)父组件@State变量更新之后,会遍历更新所有依赖他的系统组件和状态变量(@Link包装类)
  2. 通知@Link包装类更新后,子组件中所有依赖@Link状态变量的系统组件都会被通知更新。依次实现父组件对子组件的状态数据同步
7.4.3:@Link的更新
  1. @Link更新之后,调用父组件的@State包装类的set方法(创建一个键值对)将更新后的数值同步回父组件
  2. 子组件@Link和父组件@State分别遍历依赖的系统组件,进行对应的UI更新。 以此实现子组件@Link同步回父组件@State

8:@Provide @Consume

8.1:概念

  • @Provide装饰和@Consume装饰器 => 特殊的@State => @Link
  • @Provide和@Consume应用于后代组件的双向数据同步,应用与状态数据在多个层级之间传递的场景。不同与之前提到的父子组件之间通过命名参数机制传递@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递
  • @Provide装饰的变量在祖先组件中,可以理解为被提供给后代的状态变量
  • @Consume装饰的变量在后代组件中,去绑定(消费)祖先组件提供的变量

8.2:特性

  1. @Provide装饰的状态变量自动对其所有的后代组件可用,即该变量被"Provide"给他的后代组件,由此可见,@Provide方便之处在于不需要开发者多次在组件之间传递

  2. 后代通过使用@Consume去取@Provide提供的变量,建立在@Provide与@Consume之间的双向数据同步,与@State/@Link 不同的是,前者在多层次的父子组件之间传递

  3. @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定, 建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常

  4. ps:toString( ) String( ) toFixed( ) 强制类型转换 console.log( number + ‘ ’ ) 隐式类型转换

  5.      /*    通过相同的变量名绑定
          *      祖先组件: @Provide a:numer = 0
          *      后代组件: @Consume a:number √
          *      通过相同的变量别名绑定
          *      祖先组件: @Provide('a') b:number = 0
          *      后代组件: @Consume('a') c:number √
          *
          *      祖先组件: @Provide('a') b:number = 0
          *      后代组件: @Consume('b') b:number ×
          *      @Provide和@Consume通过相同的变量名或者相同的变量别名
          *      绑定时,@Provide装饰的变量和@Consume的变量是一对多
          *      的关系。不允许出现在一个组件内,包括其子组件中声明的
          *      多个同名或同别名的@Provide装饰的变量,
          *      @Provide的属性名或别名需要唯一且确定,如果声明多个同名或者
          *      同别名的@Provide装饰的变量,会发生运行时报错
          *      祖先组件 @Provide a:numer = 0 | @Provide('a') b:number = 0
          *      后代组件 @Provide a:numer = 0 | @Provide('a') b:number  不允许
          */
    

8.3:@Provide与@Consume

    /* @Provide
     * @State的规则同样适用于@Provide,
     * 差异为@Provide还作为多层后代的同步源
     * @Provide拥有参数,别名:常量字符串,可选。如果指定了别名,则通过别名绑定;
     *                  如果未指定别名,则通过变量名绑定
     * 同步类型为双向同步,从@Provide变量到所有@Consume变量以及相反方向的数据同步
     *
     * 允许装饰的变量类型: 同@State
     *
     * 类型与初始值:必须指定类型和初始值
     *
     *@Consume
     *@Consume拥有参数:别名,如果提供了别名给@Consume,则必须有@Provide
     *                变量拥有其形同的别名才能匹配成功;否则,则需要变量名相同才能匹配成功
     * 同步类型:双向同步,从@Provide变量到所有的@Consume变量,以及相反的方法
     *          双向同步的操作与@State和@Link相同
     *
     * 允许装饰的变量类型:同@State一致
     *
     * 类型与初始化:必须指定类型,建议与同名或同别名@Provide相同
     *            禁止本地初始化
     *
     *
     * @Provide从父组件初始化和更新 作为子组件
     *  初始化可选,允许从父组件中的(常规变量.@Stata......)
     * 用于初始化子组件  命名参数机制
     *   可用于初始化@State、@Link、@Prop、@Provide
     * 与父组件同步:无
     * 和后代组件中的@Consume双向同步
     *
     * @Consume 不允许从父组件去初始化。
     *          只能通过相同变量名或者相同变量别名从@Provide初始化
     * 用于初始化其他子组件:允许,可以初始化@State、@Link、@Prop、@Provide
     *
     * 观察变化:与@State、@Prop、@Link一致
     */

8.4:框架行为

  1. 初次渲染
    • @Provide装饰的变量会以map(包含键值对的对象)的形式,传递给当前@Provide所属组件的所有子组件
    • 子组件中如果使用@Consume变量,这会在此map(包含键值对的对象)中查找是否有该变量名/别名对应的@Provide变量,如果查找不到那么框架抛出JS ERROR(异常)
    • 在初始化@Consume变量时,和@State/@Link流程相同,@Consume变量会保存在map中查找到的@Provide,并将自己注册给@Provide
  2. 当@Provide装饰的数据变化时
    • 通过初次渲染可知,子组件@Consume已把自己注册给父组件,父组件@Provide变量变更之后,会遍历更新所有依赖它的状态变量(@Consume)和系统组件
    • 通知@Consume更新之后,子组件所依赖@Consume系统组件都会被通知更新依次实现@Provide对@Consume的数据同步
  3. 当@Consume装饰的数据变化时
    • 通过初次渲染的步骤可知,子组件@Consume持有@Provide的实例,在@Consume更新后调用@Provide的更新方法,将更新的数值同步回@Provide依次实现@Consume向@Provide的同步更新

9:@ObjectLink @Observed

9.1:概念

  • @Observed装饰器和@ObjectLink:嵌套类对象属性变量
  • 日常开发中应用根据开发需求,会去封装自己的数据模型(DataModel),对于多层嵌套的情况,比如数组项class,class嵌套class,二维数组,第二层无法观察到,这就需要@Observed和@ObjectLink
  • @ObjectLink和@Observed类装饰器用于涉及嵌套对象或数组场景中双向数据同步

9.2:限制条件

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

9.3:@Observed和@ObjectLink

9.3.1:@Observed
  • 类装饰器,装饰class,需要放在class定义,使用new创建对象

  • @ObjectLink修饰变量类型,必须被@Observed装饰的class实例,必须指定类型,不支持简单类型,如果想要装饰简单类型,使用@Prop

  •       /*
           * @Observed
           * class Item{}
           * @ObjectLink _object:Item
           * 允许@ObjectLink装饰的变量属性赋值
           * this._object.title = '....'
           * 不允许@ObjectLink装饰的数据自身赋值
           * this._object = new Item(....) ×
           */
    
9.3.2:@ObjectLink
  • @ObjectLink装饰的变量不能被赋值,如果想要使用赋值操作,使用@Prop(@Prop装饰的变量是深拷贝)
  • @ObjectLink装饰的变量和数据源关系是双向同步,**@ObjectLink装饰变量相当于指向数据源的指针。**禁止对@ObjectLink装饰的变量赋值,如果一旦发生了@ObjectLink装饰的变量的赋值,则同步链将被打断。
  • 因为@ObjectLink装饰的变量通过数据源(Object)引用来初始化。对于实现双向同步的@ObjectLink,赋值相当于更新父组件中的数组项或者class的属性,TS/JS不能实现,会发生运行时的报错

9.3:传递/访问规则

  • @ObjectLink 从父组件初始化 必须指定
  • 类型必须是@Observed装饰的class
  • 初始化的数据需要是数组项或者class属性
  • 同步源的class或者数组必须是@State、@Link、@Provide,@Consume或者@ObjectLink装饰的数据

9.4:框架行为

9.4.1:初次渲染
  1. @Observed装饰的class的实例会被不透明的代理对象包装,代理了class属性上的setter和getter方法 (setter:存值函数,getter:取值函数)
  2. 子组件中@ObjectLink装饰的变量从父组件初始化,接收被@Observed装饰的class的实例,@ObjectLink的包装类将自己注册给@Observed class
9.4.2:属性的更新
  1. 当@Observed装饰的class属性相关实例化对象改变时,会使用代理的setter和getter,然后遍历依赖他的ObjectLink包装类,通知数据更新

10:@Watch

10.1:概念

  1. @Watch:状态变量更改通知,@Watch应用对状态变量的监听,如果需要关注某个状态变量的值是否更改,可以使用@Watch为状态变量设置回调函数
  2. 当状态变量变化时,@Watch的回调方法将被调用。
  3. @Watch在ArkUI框架内部判断数值有无更新使用的是严格相等(===),当严格相等为false就会触发@Watch的回调
  4. 只有@Watch前状态变量能够观察到的变量变化@Watch才能观察到

10.2:使用

     /*
     * @Watch('abc') 装饰器参数必填 常量字符串
     * abc():void{}
     * 是(string) => void 自定义成员函数的方法引用
     * 可监听所有装饰器装饰的变量。不允许监听常规变量
     *
     * 写法:@State@Watch('a') a:number = 1
     *      @State@Watch('a') b:number = 2
     *      a('a','b'){}
     */

10.3:回调函数

    /* a(changePropertyName?:string) => void
     * 该函数时自定义组件的成员函数,changePropertyName
     * 是被Watch装饰的属性名。在多个状态变量绑定同一个@Watch
     * 的回调方法的时候,可以changePropertyName进行不同的逻辑处理
     * 将属性名作为字符串输入参数,不返回任何内容
     *
     * 开发者应关注性能,属性值的更新函数会延迟组件的重新
     * 因此,回调函数仅执行快速运算
     * 为了避免陷入无限循环。循环的原因可能是因为@Watch的回调方法里面
     * 直接或者间接修改了同一个状态变量。为了避免循环的产生
     * 建议不要再@Watch的回调方法里面修改当前装饰的状态变量
     *
     * */

你可能感兴趣的:(harmonyos,华为,ui)