ArkTS 装饰器总览:V1 / V2 / 通用装饰器完整学习笔记

推荐大家体验用AI编程,人工智能学习小站如下,已整理好相应高质量资源

前言 -- 人工智能学习网

适用版本:HarmonyOS 5.0.0+ / API 12+

难度:中级

预计阅读时间:30 分钟


写在前面

ArkUI 的声明式 UI 开发范式里,UI 是程序状态的运行结果------状态变了,UI 自动刷新。这套机制的核心就是装饰器

ArkUI 的装饰器体系分为三大类:

  • V1 状态管理装饰器 (15 个):经典的 @Component / @State / @Prop / @Link
  • V2 状态管理装饰器 (14 个):新一代的 @ComponentV2 / @Local / @Param / @Computed
  • 通用 UI 装饰器 (9 个):与状态管理无关的 UI 辅助装饰器,如 @Entry / @Builder / @Extend

这篇笔记会把三类装饰器逐一拆解,每个装饰器配一段最小可运行代码,让你看完就知道怎么用。


一、V1 状态管理装饰器

V1 是 ArkUI 最早的状态管理体系,至今仍是大多数项目的主力。它的核心思路是:通过装饰器声明变量的"同步方向"(单向/双向/跨层级),框架自动处理 UI 刷新。

1.1 @Component:创建自定义组件

typescript 复制代码
@Component
struct MyComponent {
  build() {
    Text('我是一个自定义组件')
  }
}

@Component 装饰的 struct 就是一个可复用的自定义组件。它必须实现 build() 方法,返回 UI 描述。

1.2 @State:基础状态变量

typescript 复制代码
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column() {
      Text(`计数:${this.count}`)
      Button('+1').onClick(() => { this.count++; })
    }
  }
}

@State 装饰的变量变化时,当前组件中依赖该变量的 UI 会自动刷新。这是最基础的状态装饰器。

1.3 @Prop:父→子单向同步

typescript 复制代码
@Component
struct Child {
  @Prop message: string;  // 接收父组件传入,子组件修改不影响父组件

  build() {
    Column() {
      Text(`子组件:${this.message}`)
      Button('修改').onClick(() => { this.message = '子组件改的'; })
    }
  }
}

@Entry
@Component
struct Parent {
  @State msg: string = '来自父组件';

  build() {
    Column() {
      Text(`父组件:${this.msg}`)
      Child({ message: this.msg })
    }
  }
}

@Prop 建立父到子的单向 数据流。父组件 @State 变化时,子组件的 @Prop 会同步更新;但子组件修改 @Prop 不会回传给父组件。

1.4 @Link:父↔子双向同步

typescript 复制代码
@Component
struct Child {
  @Link value: number;  // 双向同步,子组件修改会同步回父组件

  build() {
    Column() {
      Text(`子组件:${this.value}`)
      Button('+10').onClick(() => { this.value += 10; })
    }
  }
}

@Entry
@Component
struct Parent {
  @State val: number = 100;

  build() {
    Column() {
      Text(`父组件:${this.val}`)
      Child({ value: $val })  // 注意:@Link 用 $ 语法传递引用
    }
  }
}

@Link 建立父到子的双向 数据流。父组件用 $变量名 语法传递引用。子组件修改 @Link 变量,父组件的 @State 同步变化。

1.5 @Observed + @ObjectLink:嵌套类对象观察

typescript 复制代码
@Observed
class UserInfo {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

@Component
struct UserCard {
  @ObjectLink user: UserInfo;

  build() {
    Column() {
      Text(`姓名:${this.user.name}`)
      Text(`年龄:${this.user.age}`)
      Button('年龄+1').onClick(() => { this.user.age++; })
    }
  }
}

@Observed 标记一个类为"可观察的",@ObjectLink 在子组件中接收该类的实例。当实例的属性变化时,子组件的 UI 会自动刷新。这解决了 @State 只能观察第一层赋值、不能观察嵌套属性变化的问题。

1.6 @Provide + @Consume:跨层级双向同步

typescript 复制代码
@Entry
@Component
struct GrandParent {
  @Provide themeColor: string = '#007DFF';

  build() {
    Column() {
      Text('祖先组件')
      MiddleComponent()  // 中间组件不需要传递 themeColor
    }
  }
}

@Component
struct MiddleComponent {
  build() {
    Column() {
      ChildComponent()
    }
  }
}

@Component
struct ChildComponent {
  @Consume themeColor: string;  // 直接消费,不需要逐层传递

  build() {
    Text('后代组件').fontColor(this.themeColor)
  }
}

@Provide 在祖先组件中声明数据,@Consume 在任意后代组件中直接消费。不需要通过中间组件逐层传递 @Prop / @Link。修改任意一端,另一端同步变化。

1.7 @Watch:状态变量变化监听

typescript 复制代码
@Component
struct Counter {
  @State count: number = 0;

  @Watch('onCountChanged')
  trigger: number = 0;

  onCountChanged(): void {
    console.info(`count 变化为:${this.count}`);
  }

  build() {
    Button('+1').onClick(() => {
      this.count++;
      this.trigger++;  // 触发 @Watch 回调
    })
  }
}

@Watch 装饰一个变量,当该变量变化时,调用指定的回调函数。适合在状态变化时执行日志、统计等副作用逻辑。

typescript 复制代码
// 全局存储
AppStorage.setOrCreate('token', 'abc123');

@Entry
@Component
struct MyPage {
  @StorageLink('token') token: string = '';       // 双向同步
  @StorageProp('appName') appName: string = '';   // 单向同步(只读)

  build() {
    Column() {
      Text(`Token:${this.token}`)
      Button('修改 Token').onClick(() => {
        this.token = 'newToken456';  // 同步回 AppStorage
      })
    }
  }
}

@StorageLinkAppStorage 中的数据建立双向同步,@StorageProp 建立单向同步(只读)。适合管理全局共享的状态,如登录信息、应用配置。

@StorageLink / @StorageProp 用法类似,区别在于 LocalStorage 是页面级或 Ability 级的局部存储,作用域比 AppStorage 小。

1.10 @Track:类对象属性级更新

typescript 复制代码
@Observed
class UserModel {
  @Track name: string;   // 标记为精确追踪
  @Track age: number;
  untracked: string;     // 未标记,变化时不触发 UI 刷新

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

@Track 配合 @Observed 使用,标记哪些属性变化时应该触发 UI 更新。未标记的属性变化不会导致 UI 刷新,可以减少不必要的渲染。

1.11 @Reusable:标记组件可复用

typescript 复制代码
@Reusable
@Component
struct ListItem {
  @State title: string = '';

  build() {
    Text(this.title).fontSize(16)
  }
}

@Reusable 标记的组件在列表滚动时可以被复用,减少组件创建和销毁的开销。适合长列表场景。


二、V2 状态管理装饰器

V2 是 ArkUI 推出的新一代状态管理体系,设计上更严格、更清晰,引入了计算属性、事件输出、精确监听等 V1 缺失的能力。V1 和 V2 可以混用,但同一个组件内不能混用。

2.1 @ComponentV2:创建自定义组件(V2)

typescript 复制代码
@ComponentV2
struct MyComponent {
  build() {
    Text('V2 组件')
  }
}

V2 组件用 @ComponentV2 替代 @Component。V2 组件内部只能使用 V2 装饰器(@Local / @Param 等),不能使用 @State / @Prop 等 V1 装饰器。

2.2 @Local:组件内部状态

typescript 复制代码
@ComponentV2
struct Counter {
  @Local count: number = 0;  // 仅组件内部可修改

  build() {
    Column() {
      Text(`计数:${this.count}`)
      Button('+1').onClick(() => { this.count++; })
    }
  }
}

@Local 是 V2 版本的 @State,表示组件内部状态。与 @State 的区别在于:@Local 变量只能在当前组件内修改,不能从外部直接赋值,语义更严格。

2.3 @Param:组件外部输入

typescript 复制代码
@ComponentV2
struct Child {
  @Param inputValue: number = 0;  // 从父组件接收,实时同步

  build() {
    Text(`接收:${this.inputValue}`)
  }
}

@ComponentV2
struct Parent {
  @Local value: number = 42;

  build() {
    Child({ inputValue: this.value })
  }
}

@Param 是 V2 版本的 @Prop,从父组件接收数据。父组件的状态变化时,@Param 自动同步。

2.4 @Once:初始化同步一次

typescript 复制代码
@ComponentV2
struct Child {
  @Param @Once snapshot: number = 0;  // 仅首次接收父组件的值,之后不同步

  build() {
    Text(`快照:${this.snapshot}`)
  }
}

@Once 修饰的 @Param 变量只在初始化时同步一次父组件的值。之后父组件的值变化,子组件不会跟着变。适合"只需初始化配置"的场景。

2.5 @Event:规范组件输出

typescript 复制代码
@ComponentV2
struct Child {
  @Param value: number = 0;
  @Event onReset: () => void = () => {};

  build() {
    Button('重置').onClick(() => { this.onReset(); })
  }
}

@ComponentV2
struct Parent {
  @Local count: number = 0;

  build() {
    Column() {
      Text(`计数:${this.count}`)
      Child({ value: this.count, onReset: () => { this.count = 0; } })
    }
  }
}

@Event 声明组件的输出回调。子组件通过调用 @Event 函数通知父组件。在 V2 中,@Param + @Event 替代了 V1 的 @Link 双向同步------父组件传数据用 @Param,子组件通知父组件用 @Event,数据流向更清晰。

2.6 @Provider + @Consumer:跨层级双向同步

typescript 复制代码
@ComponentV2
struct Ancestor {
  @Provider() themeColor: ResourceColor = '#007DFF';

  build() {
    Column() {
      Text('祖先')
      Descendant()
    }
  }
}

@ComponentV2
struct Descendant {
  @Consumer() themeColor: ResourceColor = '#333333';

  build() {
    Column() {
      Text('后代').fontColor(this.themeColor)
      Button('改色').onClick(() => { this.themeColor = '#FF4444'; })
    }
  }
}

V2 版本的 @Provide / @Consume@Provider()@Consumer() 需要带括号调用。双向同步,任意一端修改另一端跟随。

2.7 @Monitor:状态变量修改异步监听

typescript 复制代码
@ComponentV2
struct MyComponent {
  @Local count: number = 0;

  @Monitor('count')
  onCountChange(monitor: IMonitor): void {
    monitor.dirty.forEach(path => {
      console.info(`@Monitor: ${path} 变化了`);
    });
  }

  build() {
    Button('+1').onClick(() => { this.count++; })
  }
}

@Monitor 是 V2 版本的 @Watch,但更强大:可以监听多个变量、获取变化路径、获取变化前后的值。回调是异步执行的。

2.8 @SyncMonitor:状态变量修改同步监听

typescript 复制代码
@ComponentV2
struct MyComponent {
  @Local value: number = 0;

  @SyncMonitor('value')
  onValueChange(monitor: IMonitor): void {
    // 同步执行,在 UI 刷新之前
    console.info(`@SyncMonitor: value 变化为 ${this.value}`);
  }

  build() {
    Button('+1').onClick(() => { this.value++; })
  }
}

@SyncMonitor@Monitor 功能相同,但回调是同步执行的,在 UI 刷新之前触发。适合需要在 UI 更新前执行前置逻辑的场景。

2.9 @Computed:计算属性

typescript 复制代码
@ComponentV2
struct Cart {
  @Local price: number = 4999;
  @Local quantity: number = 1;

  @Computed
  get total(): number {
    return this.price * this.quantity;
  }

  build() {
    Column() {
      Text(`总价:¥${this.total}`)
      Button('数量+1').onClick(() => { this.quantity++; })
    }
  }
}

@Computed 声明一个计算属性。当依赖的 @Local / @Trace 变量变化时,计算属性自动重新求值。计算属性有缓存------依赖不变时不会重复计算。

2.10 @ObservedV2 + @Trace:可观察类与属性标记

typescript 复制代码
@ObservedV2
class CartItem {
  name: string;
  @Trace quantity: number;  // 标记为可观察
  @Trace price: number;
  untracked: string;        // 未标记,变化不触发 UI

  constructor(name: string, price: number, quantity: number) {
    this.name = name;
    this.price = price;
    this.quantity = quantity;
  }
}

@ObservedV2 标记类为可观察(V2 版本的 @Observed)。@Trace 标记类中哪些属性变化时应该触发 UI 刷新。与 V1 的 @Observed 相比,V2 是属性级精确追踪 ------只有标记了 @Trace 的属性变化才会触发更新,V1 是整个对象替换才触发。

2.11 @Type:标记类属性的类型

typescript 复制代码
@ObservedV2
class Order {
  @Type(CartItem)
  item: CartItem;  // 标记属性的类型,用于序列化/反序列化

  @Trace status: string;
  constructor(item: CartItem, status: string) {
    this.item = item;
    this.status = status;
  }
}

@Type 标记类属性的具体类型,主要用于持久化(如 Preferences / RDB 存储)时的序列化和反序列化。框架需要知道属性的类型才能正确还原对象。

2.12 @ReusableV2:标记组件可复用(V2)

typescript 复制代码
@ComponentV2
@ReusableV2
struct ListItem {
  @Param title: string = '';

  build() {
    Text(this.title).fontSize(16)
  }
}

V2 版本的 @Reusable,标记组件可复用。


三、通用 UI 装饰器

通用装饰器与状态管理无关,用于 UI 结构封装、样式复用、参数校验等。

3.1 @Entry:标记页面入口

typescript 复制代码
@Entry
@Component
struct MyPage {
  build() {
    Text('这是一个页面')
  }
}

@Entry 标记一个自定义组件为页面入口。每个页面(路由对应的 .ets 文件)只能有一个 @Entry

3.2 @Builder:自定义构建函数

typescript 复制代码
@Entry
@Component
struct MyPage {
  @Builder
  CardHeader(title: string, subtitle: string) {
    Column() {
      Text(title).fontSize(20).fontWeight(FontWeight.Bold)
      Text(subtitle).fontSize(14).fontColor('#999')
    }
  }

  build() {
    Column() {
      this.CardHeader('标题一', '副标题一')
      this.CardHeader('标题二', '副标题二')
    }
  }
}

@Builder 将一段 UI 描述封装为方法,可以接收参数,在多处复用。类似 Vue 的"渲染函数"或 React 的"自定义 Hook 返回 JSX"。

3.3 @LocalBuilder:维持组件关系

typescript 复制代码
@Component
struct MyPage {
  @State count: number = 0;

  @LocalBuilder
  MyContent() {
    // this.count 可以正确访问当前组件的状态
    Text(`计数:${this.count}`)
  }

  build() {
    Column() {
      this.MyContent()
    }
  }
}

@LocalBuilder@Builder 功能类似,区别在于 this 的指向。@Builderthis 指向可能丢失,@LocalBuilder 始终指向当前组件,可以安全访问组件的状态变量和方法。

3.4 @BuilderParam:引用 @Builder 函数

typescript 复制代码
@Component
struct CustomContainer {
  @BuilderParam header: () => void;
  @BuilderParam content: () => void;

  build() {
    Column() {
      this.header()
      this.content()
    }
  }
}

@Entry
@Component
struct MyPage {
  @Builder
  MyHeader() { Text('标题').fontSize(18) }

  @Builder
  MyContent() { Text('内容').fontSize(14) }

  build() {
    CustomContainer({
      header: this.MyHeader,
      content: this.MyContent
    })
  }
}

@BuilderParam 在自定义组件中声明一个"槽位",调用方可以传入任意 @Builder 函数填充。类似 Vue 的具名插槽。

3.5 @Styles:定义组件重用样式

typescript 复制代码
@Styles
function cardBoxStyle() {
  .width('100%')
  .padding(16)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
}

@Entry
@Component
struct MyPage {
  build() {
    Column() {
      Text('卡片 A')
    }.cardBoxStyle()

    Column() {
      Text('卡片 B')
    }.cardBoxStyle()
  }
}

@Styles 封装一组通用属性(width、height、padding、margin、backgroundColor 等所有组件都有的属性),不能包含组件特有属性(如 Text 的 fontSize)。

3.6 @Extend:定义扩展组件样式

typescript 复制代码
@Extend(Text)
function titleStyle() {
  .fontSize(20)
  .fontWeight(FontWeight.Bold)
  .fontColor('#333333')
  .maxLines(1)
  .textOverflow({ overflow: TextOverflow.Ellipsis })
}

@Entry
@Component
struct MyPage {
  build() {
    Column() {
      Text('标题一').titleStyle()
      Text('标题二').titleStyle()
    }
  }
}

@Extend特定系统组件 (如 Text、Button)定义一组扩展样式,可以包含组件特有属性。比 @Styles 更灵活,但只能用于指定的组件类型。

3.7 @AnimatableExtend:定义可动画属性

typescript 复制代码
@AnimatableExtend(Text)
function animatableFontSize(size: number) {
  .fontSize(size)
}

@Component
struct AnimatableExample {
  @State fontSize: number = 16;

  build() {
    Column() {
      Text('动画文字')
        .animatableFontSize(this.fontSize)
        .onClick(() => {
          this.fontSize = this.fontSize === 16 ? 32 : 16;
        })
    }
    .animation({ duration: 400, curve: Curve.EaseInOut })
  }
}

@AnimatableExtend 定义一个可参与动画过渡的自定义属性。当属性值变化时,框架会按动画曲线平滑过渡,而不是瞬间跳变。

3.8 @Require:校验构造传参

typescript 复制代码
@Component
struct RequiredChild {
  @Require @Prop requiredProp: string;   // 必传
  @Prop optionalProp: string = '默认值';  // 可选

  build() {
    Column() {
      Text(`必传:${this.requiredProp}`)
      Text(`可选:${this.optionalProp}`)
    }
  }
}

@Entry
@Component
struct MyPage {
  build() {
    // RequiredChild()  // 编译报错:缺少 requiredProp
    RequiredChild({ requiredProp: '必须传入' })  // 正确
  }
}

@Require 标记一个 @Prop / @Link 为必传参数。如果调用方未传,编译阶段就会报错,而不是运行时出现 undefined。

3.9 @Env:环境变量

typescript 复制代码
@Entry
@Component
struct MyPage {
  @Env('sys.language') sysLanguage: string = 'zh';

  build() {
    Column() {
      Text(`系统语言:${this.sysLanguage}`)
    }
  }
}

@Env 读取系统环境变量(如语言、颜色模式、屏幕方向等)。当系统环境变化时(如用户切换语言),@Env 变量自动更新并触发 UI 刷新。


四、V1 vs V2 对比速查

V1 装饰器 V2 装饰器 用途 关键差异
@Component @ComponentV2 创建组件 V2 内部只能用 V2 装饰器
@State @Local 内部状态 @Local 语义更严格,仅内部可改
@Prop @Param 外部输入 @Param 不支持默认值修改
@Link @Param + @Event 双向同步 V2 拆分为输入和事件,方向更清晰
@Provide @Provider() 提供数据 V2 需带括号
@Consume @Consumer() 消费数据 V2 需带括号
@Watch @Monitor 变化监听 @Monitor 异步,支持多变量和路径
- @SyncMonitor 同步监听 V2 新增,UI 刷新前同步触发
- @Computed 计算属性 V2 新增,带缓存
@Observed @ObservedV2 可观察类 V2 配合 @Trace 实现属性级追踪
@ObjectLink @Trace 属性观察 V2 是属性级标记,更精确
- @Once 仅同步一次 V2 新增
- @Type 类型标记 V2 新增,用于序列化
@Reusable @ReusableV2 组件复用 V2 版本

五、工程结构

复制代码
ArkTS装饰器总览/
├── pages/
│   ├── V1Decorators.ets         # V1 状态管理装饰器完整示例
│   ├── V2Decorators.ets         # V2 状态管理装饰器完整示例
│   └── CommonDecorators.ets     # 通用 UI 装饰器完整示例
└── preview.html                 # 网页预览(4 个 Tab 页可交互)

在 DevEco Studio 中运行

  1. 新建 Empty Ability 工程
  2. 将三个 .ets 文件放入 entry/src/main/ets/pages/
  3. resources/base/profile/main_pages.json 中注册:
json 复制代码
{
  "src": [
    "pages/V1Decorators",
    "pages/V2Decorators",
    "pages/CommonDecorators"
  ]
}
  1. 运行查看效果

网页预览

用浏览器打开 preview.html,底部有 4 个 Tab 页:V1 装饰器、V2 装饰器、通用装饰器、总览对比。每个页面都可以交互操作,观察状态变化的效果。


六、选择建议

新项目用 V1 还是 V2?

如果项目刚启动,建议直接用 V2。V2 的数据流更清晰(输入/输出分离)、计算属性减少冗余状态、属性级追踪减少不必要的渲染。

老项目要不要迁移?

不急着全部迁移。V1 和 V2 可以在同一项目中混用(不同组件之间)。建议在新增功能时尝试 V2,逐步替换核心组件。同一个组件内不能混用 V1 和 V2 装饰器。

通用装饰器用哪个版本?

@Builder / @Extend / @Styles 等通用装饰器与 V1/V2 无关,任何版本都可以使用。@Entry 是页面入口标记,也与版本无关。


七、常见问题

Q:@State 和 @Local 到底有什么区别?

A:功能上都能触发 UI 刷新。@Local 的语义是"仅组件内部可修改"------外部不能通过构造参数修改它。@State 没有这个限制。在 V2 组件中只能用 @Local

Q:@Link 和 @Param + @Event 有什么区别?

A:@Link 是双向绑定,父和子都持有同一引用,任意一端修改另一端自动同步。@Param + @Event 把输入和输出拆开------父组件传数据用 @Param,子组件通知父组件用 @Event。后者的数据流更单向、更可预测。

Q:@Observed + @ObjectLink 和 @ObservedV2 + @Trace 有什么区别?

A:V1 的 @Observed 需要整个对象被替换或重新赋值才能触发更新,@ObjectLink 只是在子组件中建立观察。V2 的 @Trace 是属性级的------只修改某个标记了 @Trace 的属性就能触发更新,不需要替换整个对象。

Q:@Builder 和 @LocalBuilder 该用哪个?

A:如果 Builder 内部需要访问组件的 this(状态变量、方法),用 @LocalBuilder。如果只是纯粹的 UI 结构封装,不依赖组件状态,用 @Builder 即可。


总结

ArkUI 的装饰器体系虽然数量多(38 个),但核心逻辑只有三条线:

  1. 状态在哪里@State / @Local(内部状态)、@Prop / @Param(外部输入)、@Provide / @Provider(跨层级提供)
  2. 状态怎么变@Link / @Param+@Event(双向)、@Watch / @Monitor(监听变化)、@Computed(计算派生)
  3. 状态变了谁刷新@Observed+@ObjectLink / @ObservedV2+@Trace(类对象属性级追踪)

通用装饰器则是 UI 层面的辅助工具------封装结构(@Builder)、复用样式(@Extend / @Styles)、校验参数(@Require)、读取环境(@Env)。

掌握这些,ArkTS 声明式 UI 的全貌就清晰了。

推荐大家体验用AI编程,人工智能学习小站如下,已整理好相应高质量资源

前言 -- 人工智能学习网

推荐大家体验用AI编程,人工智能学习小站如下,已整理好相应高质量资源

前言 -- 人工智能学习网

推荐大家体验用AI编程,人工智能学习小站如下,已整理好相应高质量资源

前言 -- 人工智能学习网

相关推荐
GLDbalala1 小时前
GPU PRO 5 - 5.2 glTF: Designing an Open-Standard Runtime Asset Format笔记
笔记
春生野草2 小时前
个人笔记——栈
笔记
chnyi6_ya2 小时前
论文阅读笔记 | One Sentence, One Drama: 基于多智能体系统的个性化短剧生成
论文阅读·笔记
周倦岚2 小时前
ArkTS中的MVC、MVP、MVVM
华为·mvc·harmonyos
ysa0510302 小时前
【板子】短序列dp(换成维护更小常数维度的dp)
c++·笔记·算法·板子
特立独行的猫A2 小时前
Node.js 三方库移植到 OpenHarmony 鸿蒙PC:一篇实操指南
harmonyos
达子6663 小时前
第16章_HarmonyOs开发图解之 音频
华为·音视频·harmonyos
程序员黑豆3 小时前
鸿蒙应用开发之双向绑定实战:从 V1 到 V2 的完整迁移指南
前端·harmonyos
Dory_Youth4 小时前
联想笔记本电脑失灵
运维·笔记·电脑