JHHK

欢迎来到我的个人网站
行者常至 为者常成

装饰器和UI描述

目录

学习UI开发,一般关注以下三个问题:

1)界面和控件如何编写;

2)状态管理和组件间的数据传递如何实现;

3)用户交互时,如何做到界面渲染控制。

在展开讲界面和控件编写前,本文先介绍两个UI相关的核心概念:装饰器、UI描述。

装饰器:
用于装饰类、结构、方法以及变量,并赋予其特殊的含义。
@Entry、@Component和@State都是装饰器

@Entry装饰器

@Entry表示该自定义组件为入口组件
@Entry 关键字装饰,表示当前组件是UI页面的入口;

@Component装饰器:自定义组件

@Component表示自定义组件,

@Builder/@BuilderParam

特殊的封装UI描述的方法,细粒度的封装和复用UI描述;
@Builder修饰的方法叫 自定义构建函数
@Builder修饰的方法方法传参有两种方式:值传递和引用传递

一、@Builder


class Tmp {
  paramA1: string = ''
}


@Entry
@Component
struct Day3 {
  // 状态变量
  @State label: string = 'Hello';

  // 值传递
  @Builder
  overBuilder(param: string) {
    Row() {
      Text(`值传递: ${param}`)
    }
  }

  // 引用传递,需要是对象类型
  // 只能传一个参数,传两个参数当数据改变时不会自定刷新UI
  @Builder
  overBuilder2(param:Tmp){
    Row() {
      Text(`引用传递: ${param.paramA1}`)
    }
  }

  // 可以使用$$来表明这是一个引用传递
  @Builder
  overBuilder3($$:Tmp){
    Row() {
      Text(`引用传递: ${$$.paramA1}`)
    }
  }


  build() {
    Column({space:5}) {
      Text(`Lable:${this.label}`)

      Text('点击修改Label的值').onClick(()=>{
        // 当label的值发生改变时,引用传递的自定义构建函数的UI会自动刷新
        this.label = '你好'
      })
        .width(200).height(60)
        .backgroundColor(Color.Orange)
        .textAlign(TextAlign.Center)


      Divider()
        .width('100%').height(2)
        .backgroundColor(Color.Red)

      // 值传递的UI不会自动刷新
      this.overBuilder(this.label)
      
      // 引用传递的UI会自动刷新
      this.overBuilder2({paramA1:this.label})
      this.overBuilder3({paramA1:this.label})
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Start)
    .padding(20)
  }
}

二、@BuilderParam

BuilderParam允许在自定义组件外部传入UI逻辑,类似slot占位.

1、参数初始化组件

@Component
struct Child {
  label: string = `Child`
  @Builder customBuilder() {}
  @Builder customChangeThisBuilder() {}
  
  // 设置默认值
  @BuilderParam customBuilderParam: () => void = this.customBuilder;
  
  // 设置默认值
  @BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;

  build() {
    Column() {
      this.customBuilderParam()
      this.customChangeThisBuilderParam()
    }
  }
}

@Entry
@Component
struct Parent {
  label: string = `Parent`

  @Builder componentBuilder() {
    Text(`${this.label}`)
  }

  build() {
    Column() {
      this.componentBuilder()
      Child({ customBuilderParam: this.componentBuilder, customChangeThisBuilderParam: ():void=>{this.componentBuilder()} })
    }
  }
}

注意this指向,普通函数的this指向和箭头函数的this指向是不同的

2、尾随闭包初始化组件

此场景下自定义组件内有且仅有一个使用@BuilderParam装饰的属性。(如果有两个BuilderParam装饰的属性,会报错)
此场景下自定义组件不支持使用通用属性。

@Component
struct Child2 {
  label: string = `Child`
  @Builder customBuilder() {}
  @BuilderParam customBuilderParam: () => void = this.customBuilder;
  
  build() {
    Column() {
      this.customBuilderParam()
    }
  }
}
// 初始化
 Child2(){
  this.componentBuilder()
}

@Styles/@Extend

@Styles

@Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。
通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式。
@Styles方法不支持参数
组件内@Styles的优先级高于全局@Styles。

// 定义在全局的@Styles封装的样式
// 全局要加function关键字
// 全局【只能在当前文件内使用,不支持export。】
@Styles function globalFancy  () {
  .width(150)
  .height(100)
  .backgroundColor(Color.Pink)
}

@Entry
@Component
struct FancyUse {
  @State heightValue: number = 100
  // 定义在组件内的@Styles封装的样式
  @Styles fancy() {
    .width(200)
    .height(this.heightValue)
    .backgroundColor(Color.Yellow)
    .onClick(() => {
      this.heightValue = 200
    })
  }

  build() {
    Column({ space: 10 }) {
      // 使用全局的@Styles封装的样式
      Text('FancyA')
        .globalFancy()
        .fontSize(30)
      // 使用组件内的@Styles封装的样式
      Text('FancyB')
        .fancy()
        .fontSize(30)
    }
  }
}

@Extend

扩展已有组件(三方或者内置组件)样式;
我们尝试将公用的属性设置抽离,@Extend将样式组合复用,示例如下。

@Extend(Text) function fancyText(weightValue: number, color: Color) {
  .fontStyle(FontStyle.Italic)
  .fontWeight(weightValue)
  .backgroundColor(color)
}

@Entry
@Component
struct FancyUse {
  @State label: string = 'Hello World'

  build() {
    Row({ space: 10 }) {
      Text(`${this.label}`)
        .fancyText(100, Color.Blue)
      Text(`${this.label}`)
        .fancyText(200, Color.Pink)
      Text(`${this.label}`)
        .fancyText(300, Color.Orange)
    }.margin('20%')
  }
}

可以看到,之前重复的fontStyle、fontWeight、backgroundColor都被fancyText一个属性替换了。

@State装饰器:组件内状态

一、基本用法

@State装饰的变量,或称为状态变量,一旦变量拥有了状态属性,就和自定义组件的渲染绑定起来。
@State装饰器的作用范围仅仅在当前组件,当状态改变时,UI会发生对应的渲染改变
只能从组件内部访问,在声明时必须指定其类型和本地初始化。
可以作为@prop和@Link的数据源

可以观察到哪些变化:
1、当装饰的数据类型为boolean、string、number类型时,可以观察到数值的变化。
2、当装饰的数据类型为class或者Object时,可以观察到自身的赋值的变化,和其属性赋值的变化,即Object.keys(observedObject)返回的所有属性,无法观察到属性中的嵌套属性的变化。
3、当装饰的对象是array时,可以观察到数组本身的赋值和添加、删除、更新数组的变化。

二、本质

1、说明

左边是被@State修饰的person变量
右边是没有@State修饰的person变量
从图片可以看出被@State修饰的person被包装成一个proxy对象,绑定了这个对象,就能感知变化刷新UI
而没有被@State修饰的person只是一个普通的对象

2、在父子组件中

 // 父组件中
 struct PageView {
  @State
  person:Person = new Person('小明', 16)
  
  build(){
    Column(){
      Text(`${this.person.name} | ${this.person.age}`)
      PersonView({p:this.person})
    }
  }
 }
  
  
 // 子组件中
 struct PersonView {
    @State
    p:Person = new Person('xx',0)
    build(){
        Column(){
          Text(`${this.p.name} | ${this.p.age}`)
        }
    }
 }

当父组件将person传递给子组件中的p时,父子组件中使用的是同一个proxy对象,所以当person对象的属性发生变化时,父子组件都会刷新。
当父组件重新赋值时,子组件是监听不到的。重新赋值后父组件person的属性发生变化,子组件也就监听不到了,因为已经不是同一proxy对象。

问题
当子组件不使用@State时,父组件中person属性发生变化,子组件UI是否刷新?
不刷新,虽然父子组件绑定的是同一个proxy对象,但子组件没有使用@State

子组件中p属性发生变化,父组件UI是否刷新?
会刷新,父组件使用的是@State,又因为父子组件绑定的是同一个proxy对象,且proxy对象的属性发生变化,会触发父组件的UI刷新

3、箭头函数

Person对象下有两个方法 changeName 和 changName2

export class Person {
  name:string
  age:number

  constructor(name: string = '', age:number = 0) {
    this.name = name
    this.age = age
  }

  changeName = ()=>{
    this.name = "塔罗"
  }

  changeName2(){
    this.name = "塔罗2"
  }
}

组件内代码如下

DayCell({title:"箭头函数"}){
  Text(`${this.person.name} | ${this.person.age}`)

  Row(){
    // 没有触发UI刷新
    DayButton({text:'修改属性(箭头函数)',clickEvent:()=>{
      this.person.changeName()
    }})

    // 会触发UI刷新
    DayButton({text:'修改属性(正常函数)',clickEvent:()=>{
      this.person.changeName2()
    }})
  }
  .width('100%').margin({top:10})
  .justifyContent(FlexAlign.SpaceAround)
}

官方解释:
箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
所以在该场景下, changeName中的this指向person对象,而不是被装饰器@State代理的状态变量(proxy对象)。

我们通过调试也会发现当断点到changeName方法内时,this是一个普通的person实例对象
当我们断点到changeName2时,this是一个proxy对象

箭头函数的 this 是固定的(定义时决定),普通方法的 this 是动态的(调用时决定)。

一、@Props装饰器

@Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。
1、当父组件中的数据源更改时,与之相关的@Prop装饰的变量都会自动更新。
2、@Prop变量允许在本地修改和初始化,但修改后的变化不会同步回父组件
3、@Prop装饰器不能在@Entry装饰的自定义组件中使用
4、虽然说@Props装饰的变量可由父组件初始化,但使用范围依旧只能在组件内访问。

二、@Link装饰器

@Link装饰器与@Props装饰器的用法几乎一样,区别只是@Props装饰器是父组件向子组件单向同步,而@Link装饰器是子组件和父组件双向同步。

我们平时使用@Link装饰器就和@Props差不多,有一个区别:@Link装饰器的属性不能本地初始化。

@Provide和@Consume

@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。
@Provide和@Consume之间是双向数据同步的

@Component
struct CompD {
  // @Consume装饰的变量通过相同的属性名绑定其祖先组件CompA内的@Provide装饰的变量
  @Consume reviewVotes: number;

  build() {
    Column() {
      Text(`reviewVotes(${this.reviewVotes})`)
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
    }
    .width('50%')
  }
}

@Component
struct CompC {
  build() {
    Row({ space: 5 }) {
      CompD()
      CompD()
    }
  }
}

@Component
struct CompB {
  build() {
    CompC()
  }
}

@Entry
@Component
struct CompA {
  // @Provide装饰的变量reviewVotes由入口组件CompA提供其后代组件
  @Provide reviewVotes: number = 0;

  build() {
    Column() {
      Button(`reviewVotes(${this.reviewVotes}), give +1`)
        .onClick(() => this.reviewVotes += 1)
      CompB()
    }
  }
}

@Observed 用于装饰类

@ObjectLink 用于装饰子组件内变量,这个变量是@Observed装饰的类的实例

可以观察到嵌套类的属性的变化

// @Observed修饰类
@Observed
class ClassA {
  public name: string;
  constructor(name:string) {
    this.name = name;
  }
}

@Observed
class ClassB {
  public a: ClassA;
  constructor(a: ClassA) {
    this.a = a;
  }
}

@Component
struct ViewA {
  // @ObjectLink 修饰使用@Observe修饰的类的实例
  @ObjectLink a: ClassA
  build() {
    Column(){
      Text(`ViewA内部显示:${this.a.name}`)
      Text('ViewA内部修改')
        .onClick(()=>{
          this.a.name = 'helloA'
        })
    }
  }
}

@Entry
struct TestPage {
  @State b:ClassB = new ClassB(new ClassA('tom'))
  build() {
    Column(){
      // 不会刷新,只能观察到this.b属性的变化
      Text(`外部显示:${this.b.a.name}`)

      // 会刷新
      ViewA({a:this.b.a})

      Text('修改')
        .onClick(()=>{
          this.b.a.name = 'jack';
        })
    }
  }
}

LocalStorage和AppStorage

一、LocalStorage

UI描述

注意:
1、属性方法
2、容器组件的尾随闭包
lxy:尾随闭包本质是方法,尾随闭包内放置的子组件,就是子组件的构造方法调用

tips:这里推荐使用箭头函数来作为事件的执行回调函数。
如果使用 function 匿名函数,或者使用组件成员变量,需要bind(this)保证传入的回调函数与当前组件一致

// case1: 匿名function,需要bind(this)
Button('add counter')
  .onClick(function(){
    this.counter += 2;
  }.bind(this))
  
// case2: 成员函数,需要.bind(this)
myClickHandler(): void {
  this.counter += 2;
}
...
Button('add counter')
  .onClick(this.myClickHandler.bind(this))

行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.