摘要
文章先解释装饰器不是鸿蒙特有,而是在 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");
。鸿蒙针对内置的每个装饰器,都会有不同的代码生成逻辑,以保证运行效率。