摘要
文章先解释装饰器不是鸿蒙特有,而是在 TypeScript 中就已存在且被多种框架使用,阐述其概念、类型、使用好处,还深入到装饰器底层原理,并对比鸿蒙与 TypeScript 中装饰器的不同之处。
一、装饰器的基本概念
装饰器并非鸿蒙特有,在原生 TypeScript 中就已大量使用。根据 TS 官方描述,装饰器是特殊类型的声明,可附加到类声明、方法、属性或参数上,提供元编程能力。简单说,装饰器能用简洁语法实现功能增强,简化开发者工作。例如在 Calculator 类的 add 和 minus 方法中,可使用装饰器自动打印日志。
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`calling ${String(propertyName)}: `,...args);
return method.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number) {
return a + b;
}
@Log
minus(a: number, b: number) {
return a - b;
}
}
装饰器有多种类型,包括类装饰器、方法装饰器、访问器装饰器、属性装饰器和参数装饰器,不同类型装饰器可拿到不同对象进行操作。
二、使用装饰器的好处
使用装饰器有诸多好处,它封装了常见可复用逻辑,在不改变原有业务逻辑的情况下对核心功能进行扩展,实现代码复用且符合开闭原则。在不同场景下,如类、方法、访问器、属性、参数上使用装饰器都有不同的作用。
(一)类装饰器
例如将类注册为服务。
@Service
class UserService {
// User service logic
}
function Service(constructor: Function) {
// 把当前服务注册到全局
globalEnv.registor(constructor)
}
(二)方法装饰器
如在方法上设置失败自动重试。
class NetworkService {
// 失败自动重试 3 次
@Retry(3)
fetchData() {}
}
function Retry(retries: number) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
// 重写原函数,当原请求逻辑出现异常时,自动重新调用,
// 若超过最大次数,则抛出异常
descriptor.value = function(...args: any[]) {
let attempts = 0;
while (attempts < retries) {
try {
return method.apply(this, args);
} catch (error) {
attempts ++;
console.log(`正在重试...(${attempts}/${retries}): ${propertyKey}`);
}
}
throw new Error(`已经重试${retries}次啦`);
};
};
}
(三)访问器装饰器
如自动校验输入。
class Config {
// 自动校验输入
@Validate
set url(value: string) {}
}
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set;
// 重写原方法,如果 set 值不合法,则抛出异常,否则正常 set
descriptor.set = function(value: any) {
if (!value || typeof value!== 'string' ||!value.startsWith('http')) {
throw new Error('Invalid URL');
}
originalSet.call(this, value);
}
}
(四)属性装饰器
如将数据持久化保存。
class UserPreferences {
// 将数据持久化保存
@Persist('user_theme')
theme: string;
}
function Persist(key: string) {
return function (target: any, propertyKey: string) {
const storageKey = `property_${key}`;
// 重新定义原属性,当获取该属性时,从缓存读数据
// 当设置该属性时,更新至缓存
Object.defineProperty(target, propertyKey, {
get: function() {
return localStorage.getItem(storageKey);
},
set: function(value) {
localStorage.setItem(storageKey, value);
},
enumerable: true,
configurable: true
});
}
}
(五)参数装饰器
如设置参数必传。
class ApiService {
// userId 必传
fetchUserData(@Required userId: number) {}
}
function Required(target: any, propertyKey: string, parameterIndex: number) {
const originalMethod = target[propertyKey];
// 重写该方法,当目标参数缺失时,抛出异常
target[propertyKey] = function(...args: any[]) {
if (args[parameterIndex] === undefined) {
throw new Error('参数缺失');
}
return originalMethod.apply(this, args);
};
}
三、开源框架中的装饰器
很多前端开源框架都在使用装饰器。
(一)Angular
通过@Component 装饰器将组件的模版和组件的行为逻辑相绑定。
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.css']
})
export class MyComponent {
title: string;
constructor() {
this.title = '标题';
}
setTitle(newTitle: string): void {
this.title = newTitle;
}
}
(二)NestJS
通过@Controller、@Post、@Body 等装饰器,定义 HTTP 接口响应逻辑。
@Controller('data')
export class DataController {
@Post('fetch')
fetchData(@Body() body: { name: string; age: number }): string {
return `Name: ${body.name}, Age: ${body.age}`;
}
}
(三)MobX
使用@observable、@action 等装饰器,定义组件状态以及变更状态的方法。
class Counter {
@observable count = 0;
@action add() {
this.count ++;
}
}
四、装饰器的底层原理
装饰器算是一种语法糖,在经过 TS 编译后,被装饰的类/方法/属性会自动被装饰器包裹并调用。以@Log 装饰器为例,编译前和编译后的代码有很大变化。编译后会生成__decorate
方法,该方法根据不同的入参情况(如装饰器类型、是否有属性描述符等)来实现装饰器的应用逻辑。
五、鸿蒙中的装饰器的特点
鸿蒙中的装饰器从设计上也是为了功能增强。但对于 ArkTS 自带的装饰器(如@State 等),鸿蒙底层并非通过__decorate
方法实现,而是直接将对应的装饰器编译为具有特定功能的目标代码。以@State 装饰器为例,经过编译后代码有特定的转换方式,如将@State message: string = 'Hello World';
变成this.__message = new ObservedPropertySimplePU('Hello World', this, "message");
。鸿蒙针对内置的每个装饰器,都会有不同的代码生成逻辑,以保证运行效率。