HarmonyOS之深入了解装饰器
一、TypeScript 装饰器(Decorator)
1. 概述
装饰器是 ES 装饰器提案(Decorator Proposal) 的实现,它是 TypeScript 中的实验性特性(需启用),主要用于 对类、方法、属性等添加元编程能力。
// tsconfig.json
{"experimentalDecorators": true,"emitDecoratorMetadata": true
}
2. 装饰器的类型
装饰器类型 | 目标 | 执行时机 | 可访问信息 |
---|---|---|---|
类装饰器 | 类 | 类定义时 | 构造函数 |
属性装饰器 | 类的属性 | 类定义时 | 类原型、属性名 |
方法装饰器 | 类的方法 | 类定义时 | 原型、方法名、属性描述符 |
访问器装饰器 | getter/setter | 类定义时 | 原型、访问器名、属性描述符 |
参数装饰器 | 方法参数 | 类定义时 | 原型、方法名、参数位置 |
3. 执行顺序(重要)
多个装饰器的执行顺序:
@ClassDeco
class MyClass {@PropertyDecomyProperty: string;@MethodDecomyMethod(@ParamDeco param: string) { }
}
执行顺序为:
- 参数装饰器
- 方法装饰器
- 属性装饰器
- 类装饰器
且从下往上执行多个装饰器(即先内后外)。
4. 每种装饰器的实现机制
a. 类装饰器
function Logger(constructor: Function) {console.log("类装饰器触发", constructor.name);
}
- 装饰器函数接收构造函数作为参数;
- 可以返回一个新的构造函数(替换类);
- 适合用于依赖注入、静态注册、元信息记录等场景。
b. 方法装饰器
function Log(target: any, methodName: string, descriptor: PropertyDescriptor) {const original = descriptor.value;descriptor.value = function (...args: any[]) {console.log(`调用 ${methodName} 参数:`, args);return original.apply(this, args);};
}
- 装饰的是方法;
- 可用来包装逻辑,如日志记录、性能监控、错误捕获。
c. 属性装饰器
function Watch(target: any, propertyKey: string) {console.log(`属性 ${propertyKey} 被定义在:`, target);
}
- 不能获取属性值;
- 主要用于元信息注册(如序列化字段、ORM 映射)。
d. 参数装饰器
function Inject(target: any, method: string, index: number) {console.log(`第${index}个参数被装饰`);
}
- 可用于依赖注入容器中识别参数依赖。
5. 元数据机制(emitDecoratorMetadata + reflect-metadata)
配合 emitDecoratorMetadata: true
,TS 会自动注入类型信息元数据,供运行时使用:
function LogType(target: any, key: string) {const type = Reflect.getMetadata("design:type", target, key);console.log(`${key} 类型是:`, type.name);
}
依赖 reflect-metadata
库,该库实现了 Reflect API 和元信息存储机制。
6. 本质实现流程(底层)
TypeScript 编译器做了什么?
- 识别装饰器语法;
- 按照目标(类、方法等)插入调用;
- 若开启 emitDecoratorMetadata,会插入
Reflect.metadata(...)
调用; - 所有装饰器代码 保留在运行时代码中执行。
关键特性:
- 完全运行时执行;
- 动态性强,但性能开销也存在;
- 可构建框架功能(如 NestJS、TypeORM 全靠它)。
二、ArkTS 装饰器
1. 背景与定位
ArkTS 是 OpenHarmony(鸿蒙系统)为其 UI 框架定制的 TypeScript 方言,装饰器不是用于“扩展元编程能力”,而是 用于状态声明、组件通信、生命周期管理的 DSL 语义标记。
装饰器是告诉编译器:这里是状态,这里是绑定,这里是通信入口。
2. 装饰器类型及其语义行为(编译期语义,重点)
装饰器 | 作用范围 | 编译器行为描述 |
---|---|---|
@State | 组件内部变量 | 生成 getter/setter,自动追踪值变化,触发 UI 更新 |
@Prop | 父传子属性 | 父值传入子组件,只读,单向同步 |
@Link | 父子共享属性 | 双向同步,子组件改值会同步给父组件 |
@Provide | 提供跨层级状态 | 生成上下文注入代码 |
@Consume | 使用跨层级状态 | 注入 Provide 提供的值 |
@ObservedV2 | 响应式类标记 | 深度监听类中属性 |
@Trace | 标记需监听的字段 | 用于 @ObservedV2 类内部 |
@LocalStorage | 本地状态持久化 | 编译生成持久化代码(通过本地 KV 存储) |
@AppStorage | 全局状态存储 | 跨组件共享状态,支持持久化 |
@Builder | 构建函数注册 | 编译生成可复用的 UI 构造器 |
@Styles , @Extend | 样式注入 | 编译为内部样式调用 |
3. 编译机制详解(核心)
与 TypeScript 装饰器不同,ArkTS 装饰器是编译器指令,不是运行时代码。
ArkCompiler 处理流程:
-
解析装饰器:构建 AST(抽象语法树)后,发现
@State
、@Link
等装饰器; -
绑定语义处理器:每种装饰器都关联特定代码生成逻辑;
-
生成代码逻辑:
@State
:创建 getter/setter,并绑定变化监听逻辑;@Link
:生成数据绑定结构,修改值时同时刷新父子组件;@Prop
:设置为只读,插入一段更新依赖结构;@ObservedV2
:修改类的原型链,让其属性变化也会触发通知;
-
插入 UI 更新逻辑:绑定变量变化 → 触发 UI 重新渲染 → 更新虚拟 DOM → 重新绘制。
4. 例子:编译器到底“变出了什么”
@Component
struct Counter {@State count: number = 0;build() {Text(this.count.toString());Button("Add", () => this.count++);}
}
编译器背后大致生成:
let _count = 0;
function get_count() { return _count; }
function set_count(v) {_count = v;__triggerUIUpdate(); // 插入 UI 更新代码
}
Text()
内绑定的是 getter,this.count++
实际调用的是 setter,UI 更新由编译器处理。
5. 装饰器之间的交互机制
例如:
@ObservedV2 class Address { @Trace street = ''; }
@Component struct A {@State addr: Address = new Address();
}
@Component struct B {@ObjectLink addr: Address;
}
- 编译器发现
@ObservedV2
→ 为 Address 生成监听结构; @ObjectLink
→ 绑定状态引用;- 修改
addr.street
→ 自动触发 A/B 组件 UI 更新; - 若整体替换 addr → 同步被断开(需深拷贝保护)。
6. 核心特点(和 TS 装
饰器对比)
特性 | TypeScript 装饰器 | ArkTS 装饰器 |
---|---|---|
执行时机 | 运行时执行 | 编译期处理 |
类型扩展 | 任意目标 | 仅限组件相关语义 |
灵活度 | 极高 | 受限于框架设计 |
运行开销 | 有运行期反射负担 | 无,提前生成逻辑 |
主要目的 | 元编程、通用逻辑 | 状态管理、UI更新 |
扩展方式 | 写逻辑函数 | 不能自定义,只能用框架内置装饰器 |
总结:TypeScript vs ArkTS 装饰器(深入+对比)
方面 | TypeScript 装饰器 | ArkTS 装饰器 |
---|---|---|
本质 | 运行时代码增强函数 | 编译期语义标记(类似 DSL) |
目标范围 | 类、属性、方法、参数等 | 组件状态、通信、样式、响应式对象等 |
编译器行为 | 保留运行时代码,靠 Reflect 机制 | 删除装饰器,生成 UI 状态同步逻辑 |
用户扩展性 | 可自由实现装饰器逻辑 | 受限,只能使用系统定义的装饰器 |
开发定位 | 通用语言特性 | UI DSL 编译指令 |
使用效果 | 功能灵活,逻辑自由 | 开发效率高,结构清晰,性能高,但灵活性低 |