鸿蒙学习ArkTS之基本语法<装饰器>

140 阅读11分钟

1:装饰器是什么

装饰器本质上是一种特殊类型的声明,它可以被附加到类声明、方法、属性或参数上,用来修改类的行为。简单来说,装饰器是一个函数,它接收目标对象、目标对象的属性名以及属性描述符作为参数,然后返回一个新的属性描述符或者直接修改目标对象

在 HarmonyOS 的开发中,装饰器是一种非常强大的工具,用于声明组件的行为和数据管理

状态管理指的是,管理数据变化去刷新UI的整个过程

2.装饰器

  • 组件装饰器
  • 状态管理装饰器

2.1 @Entry 这个装饰器用于标记页面的入口点,必须声明在页面的主结构上。

@Entry
@Component
struct Index {
    aboutToAppear() {
        console.log('页面初始化:Index');
    }

    build() {
        // 页面构建逻辑
        Column() {
            Text('这是主页')
                .fontSize(24)
                .margin(10)
        }
    }
}

2.2@Component - 该装饰器用于声明页面或子组件的构建结构。

@Component
struct MyComponent {
    aboutToAppear() {
        console.log('组件初始化:MyComponent');
    }

    build() {
        Row() {
            Text('Hello World')
        }
    }
}

2.3 @Builder 自定义构建函数

ArkUI提供了一种轻量的UI元素复用机制@Builder,其内部UI结构固定,仅与使用方进行数据传递,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。

@Builder装饰器有两种使用方式,

  • 分别是定义在自定义组件内部的私有自定义构建函数
  • 定义在全局的全局自定义构建函数。
// 私有
@Entry
@Component
struct BuilderDemo {
  @Builder
  showTextBuilder() {
    Text('Hello World')
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }
  @Builder
  showTextValueBuilder(param: string) {
    Text(param)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }
  build() {
    Column() {
      // 无参数
      this.showTextBuilder()
      // 有参数
      this.showTextValueBuilder('Hello @Builder')
    }
  }
}
//全局
@Builder
function showTextBuilder() {
  Text('Hello World')
    .fontSize(30)
    .fontWeight(FontWeight.Bold)
}
@Entry
@Component
struct BuilderDemo {
  build() {
    Column() {
      showTextBuilder()
    }
  }
}

2.4 @Style @Extend AttributeModifier 样式装饰器

如果每个组件的样式都需要单独设置,在开发过程中会出现大量代码在进行重复样式设置,虽然可以复制粘贴,但为了代码简洁性和后续方便维护,我们推出了可以提炼公共样式进行复用的装饰器@Styles,@Extend和AttributeModifier。

注意:

1: @Style 不能传参数 可局部和全局 不可类外

2: @Extend 可以传递参数 不可局部 可全局 不可类外

2: Extend和AttributeModifier 可以传递参数 可局部 可全局 可类外

能力@Styles@ExtendAttributeModifier
跨文件导出不支持不支持支持
通用属性设置支持支持支持
通用事件设置支持支持部分支持
组件特有属性设置不支持支持部分支持
组件特有事件设置不支持支持部分支持
参数传递不支持支持支持
多态样式支持不支持支持
业务逻辑不支持不支持支持
2.4.1 @Style 定义组件重用样式
  • 当前@Styles仅支持通用属性通用事件
  • @Styles可以定义在组件内或全局,在全局定义时需在方法名前面添加function关键字,组件内定义时则不需要添加function关键字。
  • 只能在当前文件内使用,不支持export。
  • 不支持在@Styles方法内使用逻辑组件,在逻辑组件内的属性不生效。
  • @Styles方法不能有参数,编译期会报错,提醒开发者@Styles方法不支持参数。
// 定义在全局的@Styles封装的样式
@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)
    }
  }
}
2.4.2 @Extend 用于扩展原生组件样式

支持封装指定组件的私有属性、私有事件和自身定义的全局方法。

注意:

和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。

// 正确写法
@Extend(Text) function fancy (fontSize: number) {
  .fontSize(fontSize)
}

@Entry
@Component
struct FancyUse {

  build() {
    Row({ space: 10 }) {
      Text('Fancy')
        .fancy(16)
    }
  }
}
2.4.2 stateStyles可以依据组件的内部状态的不同

@Styles仅仅应用于静态页面的样式复用,stateStyles可以依据组件的内部状态的不同,快速设置不同样式。这就是我们本章要介绍的内容stateStyles(又称为:多态样式)。

stateStyles是属性方法,可以根据UI内部状态来设置样式,类似于css伪类,但语法不同。ArkUI提供以下五种状态:

  • focused:获焦态。
  • normal:正常态。
  • pressed:按压态。
  • disabled:不可用态。
  • selected10+:选中态。
@Entry
@Component
struct MyComponent {
  @Styles normalStyle() {
    .backgroundColor(Color.Gray)
  }

  @Styles pressedStyle() {
    .backgroundColor(Color.Red)
  }

  build() {
    Column() {
      Text('Text1')
        .fontSize(50)
        .fontColor(Color.White)
        .stateStyles({
          normal: this.normalStyle,
          pressed: this.pressedStyle,
        })
    }
  }
}
2.4.3 AttributeModifier AttributeUpdater 类外可调用 支持逻辑

AttributeModifier

声明式语法引入的@Styles@Extend两个装饰器,虽然可以解决复用相同自定义样式的问题,但是使用场景存在一定局限性,如无法跨文件导出等。为此,ArkUI引入了AttributeModifier机制,可以通过Modifier对象动态修改属性。与@Styles和@Extend相比,AttributeModifier提供了更强的能力和灵活性,且在持续完善全量的属性和事件设置能力,因此推荐优先使用AttributeModifier。

AttributeUpdater

AttributeUpdater是一个特殊的AttributeModifier,除了继承AttributeModifier的能力,还提供了获取属性对象的能力。通过属性对象可以不经过状态变量,直接更新对应属性。开发者可以通过AttributeUpdater实现自定义的更新策略,进一步提高属性更新的性能。

区别:

  • AttributeModifier:常用于组件属性的初始化,可通过链式调用一次性设置多属性,如设置文本组件初始的字体大小、颜色和宽度。
  • AttributeUpdater:主要用于响应式更新,当组件状态改变时自动更新属性,如按钮点击状态改变时更新其样式。
// button_modifier.ets
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  // 可以实现一个Modifier,定义私有的成员变量,外部可动态修改
  isDark: boolean = false

  // 通过构造函数,创建时传参
  constructor(dark?: boolean) {
    this.isDark = dark ? dark : false
  }

  applyNormalAttribute(instance: ButtonAttribute): void {
    // instance为Button的属性对象,可以通过instance对象对属性进行修改
    if (this.isDark) { // 支持业务逻辑的编写
      // 属性变化触发apply函数时,变化前已设置并且变化后未设置的属性会恢复为默认值
      instance.backgroundColor('#707070')
    } else {
      // 支持属性的链式调用
      instance.backgroundColor('#17A98D')
        .borderColor('#707070')
        .borderWidth(2)
    }
  }
}



// demo.ets
import { MyButtonModifier } from './button_modifier'

@Entry
@Component
struct attributeDemo {
  // 支持用状态装饰器修饰,行为和普通的对象一致
  @State modifier: MyButtonModifier = new MyButtonModifier(true);

  build() {
    Row() {
      Column() {
        Button("Button")
          .attributeModifier(this.modifier)
          .onClick(() => {
            // 对象的一层属性被修改时,会触发UI刷新,重新执行applyNormalAttribute
            this.modifier.isDark = !this.modifier.isDark
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

import { AttributeUpdater } from '@ohos.arkui.modifier'

class MyButtonModifier extends AttributeUpdater<ButtonAttribute> {
  // 首次绑定时触发initializeModifier方法,进行属性初始化
  initializeModifier(instance: ButtonAttribute): void {
    instance.backgroundColor('#2787D9')
      .width('50%')
      .height(30)
  }
}

@Entry
@Component
struct updaterDemo {
  modifier: MyButtonModifier = new MyButtonModifier()

  build() {
    Row() {
      Column() {
        Button("Button")
          .attributeModifier(this.modifier)
          .onClick(() => {
            // 通过attribute,直接修改组件属性,并立即触发组件属性更新
            this.modifier.attribute?.backgroundColor('#17A98D').width('30%')
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

2.5 @AnimatableExtend装饰器:定义可动画属性

@AnimatableExtend装饰器用于自定义可动画的属性方法,在这个属性方法中修改组件不可动画的属性。在动画执行过程时,通过逐帧回调函数修改不可动画属性值,让不可动画属性也能实现动画效果。也可通过逐帧回调函数修改可动画属性的值,实现逐帧布局的效果。

  • 可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值可以使animation属性的动画效果生效,这个属性称为可动画属性。比如height、width、backgroundColor、translate属性,和Text组件的fontSize属性等。
  • 不可动画属性:如果一个属性方法在animation属性前调用,改变这个属性的值不能使animation属性的动画效果生效,这个属性称为不可动画属性。比如Polyline组件的points属性等。
@AnimatableExtend(Text)
function animatableWidth(width: number) {
  .width(width)
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State textWidth: number = 80;

  build() {
    Column() {
      Text("AnimatableProperty")
        .animatableWidth(this.textWidth)
        .animation({ duration: 2000, curve: Curve.Ease })
      Button("Play")
        .onClick(() => {
          this.textWidth = this.textWidth == 80 ? 160 : 80;
        })
    }.width("100%")
    .padding(10)
  }
}

2.6 @State 用于声明组件内的可变状态。当状态值改变时,UI 将自动重新渲染。(@Local替代)

z注意:

不允许在build里改变状态变量,状态管理框架会在运行时报出Error级别日志

@Entry
@Component
struct Index {
    @State message: string = 'Hello World'
@State numCount = 1
    aboutToAppear() {
        console.log(`初始消息: ${this.message}`);
    }

    build() {
        Column() {
            Button('修改消息')
                .onClick(() => {
                    this.message = '消息已修改';
  
               
                })
               // 这样是不允许的  这样会造成  应避免直接在Text组件内改变count的值
               //这样会造成Text被多执行一次
               Text(`${this.numCount++}`)
                     .width(50)
                     .height(50)
        }
    }
}

3: V2所属装饰器

V1版本装饰器 V1版本装饰,在复杂逻辑开发中出现了一些或多或少的调用异常 因此迭代出了V2版本 建议直接从V2版本开发

3.1 @ComponentV2装饰器:自定义组件

为了在自定义组件中使用V2版本状态变量装饰器的能力,开发者可以使用@ComponentV2装饰器装饰自定义组件。

@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件:

除非特别说明,@ComponentV2装饰的自定义组件将与@Component装饰的自定义组件保持相同的行为。

@ComponentV2 // 装饰器
struct Index { // struct声明的数据结构
  build() { // build定义的UI
  }
}

3.2 @Local装饰器:组件内部状态

为了实现对@ComponentV2装饰的自定义组件中变量变化的观测,开发者可以使用@Local装饰器装饰变量。 替代了@State

@Local表示组件内部的状态,使得自定义组件内部的变量具有观测变化的能力:

  • 被@Local装饰的变量无法从外部初始化,因此必须在组件内部进行初始化。
  • 当被@Local装饰的变量变化时,会刷新使用该变量的组件。
  • @Local支持观测number、boolean、string、Object、class等基本类型以及Array、Set、Map、Date等内嵌类型。
  • @Local的观测能力仅限于被装饰的变量本身。当装饰简单类型时,能够观测到对变量的赋值;当装饰对象类型时,仅能观测到对对象整体的赋值;当装饰数组类型时,能观测到数组整体以及数组元素项的变化;当装饰Array、Set、Map、Date等内嵌类型时,可以观测到通过API调用带来的变化。详见观察变化
  • @Local支持null、undefined以及联合类型。

状态管理V1使用@State装饰器定义类中的状态变量。但由于@State装饰器能够从外部初始化,因此@State无法准确表达组件内部状态不能被外面修改的语义。

@Entry
@ComponentV2
struct Index {
  @Local count: number = 0;
  @Local message: string = "Hello";
  @Local flag: boolean = false;
  build() {
    Column() {
      Text(`${this.count}`)
      Text(`${this.message}`)
      Text(`${this.flag}`)
      Button("change Local")
        .onClick(()=>{
          // 当@Local装饰简单类型时,能够观测到对变量的赋值
          this.count++;
          this.message += " World";
          this.flag = !this.flag;
      })
    }
  }
}

3.3 @ObservedV2装饰器和@Trace装饰器:类属性变化观测

为了增强状态管理框架对类对象中属性的观测能力,开发者可以使用@ObservedV2装饰器和@Trace装饰器装饰类以及类中的属性。

@ObservedV2和@Trace提供了对嵌套类对象属性变化直接观测的能力,是状态管理V2中相对核心的能力之一。在阅读本文档前,建议提前阅读:状态管理概述来了解状态管理V2整体的能力架构。

概述

@ObservedV2装饰器与@Trace装饰器用于装饰类以及类中的属性,使得被装饰的类和属性具有深度观测的能力:

  • @ObservedV2装饰器与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。
  • 被@Trace装饰器装饰的属性property变化时,仅会通知property关联的组件进行刷新。
  • 在嵌套类中,嵌套类中的属性property被@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。
  • 在继承类中,父类或子类中的属性property被@Trace装饰且该property所在类被@ObservedV2装饰时,才具有触发UI刷新的能力。
  • 未被@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。
  • @ObservedV2的类实例目前不支持使用JSON.stringify进行序列化。
@ObservedV2
class Son {
  @Trace age: number = 100;
}
class Father {
  son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
  father: Father = new Father();

  build() {
    Column() {
      // 当点击改变age时,Text组件会刷新
      Text(`${this.father.son.age}`)
        .onClick(() => {
          this.father.son.age++;
        })
    }
  }
}

注意: @Trace装饰的属性具有被观测变化的能力

@ObservedV2
class Manager {
  @Trace static count: number = 1;
}
@Entry
@ComponentV2
struct Index {
  build() {
    Column() {
      // 当点击改变count时,Text组件会刷新
      Text(`${Manager.count}`)
        .onClick(() => {
          Manager.count++;
        })
    }
  }
}

3.4 @Param:参数外部输入

为了增强子组件接受外部参数输入的能力,开发者可以使用@Param装饰器。

@Param不仅可以接受组件外部输入,还可以接受@Local的同步变化。在阅读本文档前,

作用:

  • 参数传递
  • 变化同步
@Entry
@ComponentV2
struct Index {
  @Local count: number = 0;
  @Local message: string = "Hello";
  @Local flag: boolean = false;
  build() {
    Column() {
      Text(`Local ${this.count}`)
      Text(`Local ${this.message}`)
      Text(`Local ${this.flag}`)
      Button("change Local")
        .onClick(()=>{
          // 对数据源的更改会同步给子组件
          this.count++;
          this.message += " World";
          this.flag = !this.flag;
      })
      Child({
        count: this.count,
        message: this.message,
        flag: this.flag
      })
    }
  }
}
@ComponentV2
struct Child {
  @Require @Param count: number;
  @Require @Param message: string;
  @Require @Param flag: boolean;
  build() {
    Column() {
      Text(`Param ${this.count}`)
      Text(`Param ${this.message}`)
      Text(`Param ${this.flag}`)
    }
  }
}

3.5 @Once:初始化同步一次

为了实现仅从外部初始化一次、不接受后续同步变化的能力,开发者可以使用@Once装饰器搭配@Param装饰器使用。

仅初始化数据传递 变化不传递 搭配@Param 使用

@ComponentV2
struct ChildComponent {
  @Param @Once onceParam: string = "";
  build() {
      Column() {
        Text(`onceParam: ${this.onceParam}`)
      }
  }
}
@Entry
@ComponentV2
struct MyComponent {
  @Local message: string = "Hello World";
  build() {
      Column() {
      Text(`Parent message: ${this.message}`)
      Button("change message")
        .onClick(() => {
          this.message = "Hello Tomorrow";
        })
      ChildComponent({ onceParam: this.message })
      }
  }
}

3.6 @Event装饰器 子组件属性变化通知父组件

@Entry
@ComponentV2
struct Index {
  @Local title: string = "Title One";
  @Local fontColor: Color = Color.Red;

  build() {
    Column() {
      Child({
        title: this.title,
        fontColor: this.fontColor,
        changeFactory: (type: number) => {
          if (type == 1) {
            this.title = "Title One";
            this.fontColor = Color.Red;
          } else if (type == 2) {
            this.title = "Title Two";
            this.fontColor = Color.Green;
          }
        }
      })
    }
  }
}

@ComponentV2
struct Child {
  @Param title: string = '';
  @Param fontColor: Color = Color.Black;
  @Event changeFactory: (x: number) => void = (x: number) => {};

  build() {
    Column() {
      Text(`${this.title}`)
        .fontColor(this.fontColor)
      Button("change to Title Two")
        .onClick(() => {
          this.changeFactory(2);
        })
      Button("change to Title One")
        .onClick(() => {
          this.changeFactory(1);
        })
    }
  }
}

3.7 @Provider装饰器和@Consumer装饰器:跨组件层级双向同步

@Provider和@Consumer用于跨组件层级数据双向同步,可以使得开发者不用拘泥于组件层级。

@Provider和@Consumer属于状态管理V2装饰器,所以只能在@ComponentV2中才能使用,在@Component中使用会编译报错。

@Provider和@Consumer提供了跨组件层级数据双向同步的能力。在阅读本文档前,建议提前阅读:@ComponentV2

@Entry
@ComponentV2
struct Parent {
  @Provider() str: string = 'hello';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
      Child()
    }
  }
}

@ComponentV2
struct Child {
  @Consumer() str: string = 'world';

  build() {
    Column() {
      Button(this.str)
        .onClick(() => {
          this.str += '0';
        })
    }
  }
}

总结

  • @Entry 用于启动
  • @Component 用于构造组件 (@ComponentV2替代)
  • @Builder 构造自定义函数
  • @Style @Extend AttributeModifier用于样式
  • @Local 用于数据更新出发UI更新
  • @ObservedV2 搭配 @Trace 实现属性变动触发更新
  • @Param 构造参数传递(数据同步)
  • @Once:搭配 @Param 仅初始化的时候同步
  • @Event装饰器 搭配@Param 实现 子组件数据更新 通知父组件
  • @Provider装饰器和@Consumer装饰器:跨组件层级双向同步