第 74 期 - 装饰器在鸿蒙与 TypeScript 中的应用及原理
logoFRONTALK AI/1月6日 16:31/阅读原文

摘要

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

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有