鸿蒙@装饰器揭秘:新鲜事物还是成熟技术?

0. 引言

如果你刚接触鸿蒙开发,可能对鸿蒙中五花八门的@装饰器印象特别深刻,比如页面入口@Entry、组件入口@Component、状态定义@State等等。

你不禁会问:

  • 装饰器是鸿蒙特有的新东西么?
  • 为什么普通代码加上装饰器之后,就具备了各种各样的功能?
  • 我能不能写个自己的装饰器呢?
  • 鸿蒙中的装饰器到底有何不同?

看完这篇文章,相信你对装饰器的理解更加深刻。

1. 什么是装饰器

首先,装饰器并不是鸿蒙ArkTS引入的新东西,在原生Typescript中,就已经在大量使用了。根据TS官方的描述,装饰器是一种特殊类型的声明,能够被附加到类声明、方法、属性或参数上,为开发者提供一种 元编程 的能力

简单点说人话,装饰器能够用简洁的语法实现功能增强,简化开发者重复的工作

举个例子,假设现在有个Calculator的类,其实现了add和minus方法,我们想分别打印add和minus的入参。

Typescript 复制代码
class Calculator {
    add(a: number, b: number) {
        console.log(`calling add: `, a, b)
        return a + b;
    }
    minus(a: number, b: number) {
        console.log(`calling minus: `, a, b)
        return a - b;
    }
}
const calculator = new Calculator();
calculator.add(1, 2);
// 输出 calling add:  1 2
calculator.minus(1, 2);
// 输出 calling minus:  1 2

但是我们需要手动去写log,并且针对每个方法,都要写不同的log信息(方法名不同)。现在我们可以使用装饰器来简化这一操作。

Typescript 复制代码
// 这是个装饰器函数,不要慌,后面会讲
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;
    }
}

const calculator = new Calculator();
calculator.add(1, 2);
// 输出 calling add:  1 2
calculator.minus(1, 2);
// 输出 calling minus:  1 2

只需要在对应的方法上方加上@Log,就能实现自动打印日志了。

接下来我们看看装饰器都能装饰什么

  • 类装饰器
Typescript 复制代码
function Log(constructor: Function) {
     // 类装饰器中可以拿到类的构造函数constructor
     // 你可以监视、修改,甚至返回一个新的类
    console.log(`Logging class: ${constructor.name}`);
}

@Log
class Example {
    constructor() {}
}
  • 方法装饰器
Typescript 复制代码
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 在方法装饰器中,你可以拿到
    // target:类的构造函数或原型对象
    // propertyKey:方法名(比如someMethod)
    // descriptor:属性描述符
    console.log(`Method decorated: ${propertyKey}`);
}

class Example {
    @Log
    someMethod() {}
}
  • 访问器装饰器
Typescript 复制代码
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 在属性装饰器中,你可以拿到
    // target:类的构造函数或原型对象
    // propertyKey:属性名称(比如 prop)
    // descriptor: 属性描述符
    console.log(`Accessor decorated: ${propertyKey}`);
}

class Example {
    @Log
    get prop() {}
}
  • 属性装饰器
Typescript 复制代码
function Log(target: any, propertyKey: string) {
    // 在属性装饰器中,你可以拿到
    // target:类的构造函数或原型对象
    // propertyKey:属性名称(比如 someProperty)
    console.log(`Property decorated: ${propertyKey}`);
}

class Example {
    @Log
    someProperty: string;
}
  • 参数装饰器
Typescript 复制代码
function Log(target: any, propertyKey: string, parameterIndex: number) {
    // 在参数装饰器中,你可以拿到
    // target:类的构造函数或原型对象
    // propertyKey:方法名称(比如 someMethod)
    // parameterIndex:参数的位置(比如 0)
    console.log(`Parameter decorated in method: ${propertyKey} at position: ${parameterIndex}`);
}

class Example {
    someMethod(@Log param: string) {}
}

在回到最开始的例子,我们写了个方法装饰器 ,要想打印方法的名称,使用propertyName即可。要想记录调用的入参,我们修改了原方法的属性描述符,将原方法的值(即value)用新方法代替,同时记录新方法入参,并在新方法内调用原方法(method.apply())。

Typescript 复制代码
function Log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    // target:类的构造函数或原型对象
    // propertyKey:方法名
    // descriptor:属性描述符
    const method = descriptor.value;
    descriptor.value = function (...args: any[]) {
        console.log(`calling ${String(propertyName)}: `, ...args);
        return method.apply(this, args);
    };
}

至此,你应该已经了解装饰器的基本用法了。

注意,装饰器是TS的一种能力,在JS中仍在提案阶段(见tc39_proposal_decorators).

2. 为什么要使用装饰器

通过上面的例子,相信你已经发现了使用装饰器的好处。装饰器封装了常见可复用逻辑,在不改变原有业务逻辑的情况下,对核心功能进行扩展。 不仅实现了代码复用,而且符合开闭原则(对扩展开放,对修改关闭)。

在装饰器内部,你可以

  • 替换被装饰的值(包括类、方法、参数)为一个新的值。
  • 监视被装饰的值,持久化保存 或 和其他关联场景共享这个值(如果有必要的话)。
  • 当被装饰的值被赋值之后,额外再运行些其他的逻辑。

你可以非常方便的在不破坏原有逻辑的前提下,插入新功能。

以下举一些常用的装饰器使用场景。

  • 类装饰器
Typescript 复制代码
// 将类注册为服务
@Service
class UserService {
    // User service logic
}

function Service(constructor: Function) {
     // 把当前服务注册到全局
    globalEnv.registor(constructor)
}
  • 方法装饰器
Typescript 复制代码
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}次啦`);
        };
    };
}
  • 访问器装饰器
Typescript 复制代码
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);
    }
}
  • 属性装饰器
Typescript 复制代码
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
        });
    }
}
  • 参数装饰器
Typescript 复制代码
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);
    };
}

3. 哪些开源框架也在使用装饰器

装饰器并非鸿蒙ArkTS率先使用的,相反,很多有名的前端开源框架都在使用装饰器。

  • Angular(视图框架) :通过@Component装饰器将组件的模版和组件的行为逻辑相绑定

@Component装饰器中指明组件的视图模版和样式,对应装饰的MyComponent类中实现了这个组件的属性和逻辑(例如设置标题setTitle方法),将视图与逻辑绑定。

Typescript 复制代码
@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'):定义一个控制器,用来处理路由为/data的请求
  • @Post:定义一个post方法,用来处理路由为/data/fetch的post请求
  • @Body:用于自动解析post请求中的body数据,方便业务中使用
Typescript 复制代码
@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等装饰器,定义组件状态以及变更状态的方法。
  • 使用 @observable 定义一个可观察属性,当该属性变化时,依赖该属性的组件会进行更新
  • 使用 @action 定义一个更改属性的动作,用于修改被观察属性
Typescript 复制代码
class Counter {
    @observable count = 0;

    @action add() {
        this.count++;
    }
}

可见,装饰器的使用非常广泛。在Class中添加语义化的装饰器,可以让重复功能的开发成本大幅降低,使用更加便捷。

4. 装饰器的底层原理

讲了这么多,装饰器底层到底是如何实现的?其实装饰器的底层原理非常之简单。

简单来说,装饰器算是一种语法糖,在经过TS编译后, 被装饰的类/方法/属性会自动被装饰器包裹并调用 ,来达到装饰的效果。

我们还是以最开头的@Log的例子,来看看经过TS编译后,代码发生了哪些变化

  • 编译前 index.ts
Typescript 复制代码
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;
    }
}

在经过TS编译后,以下一些非装饰器相关的内容可以忽略不用管

  • 生成了一个__spreadArray方法,因为在TS中使用到了...扩展运算符
  • class Calculator 被编译成 var Calculator(类被还原成函数了)
  • 编译后 index.js
Javascript 复制代码
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
function Log(target, propertyName, descriptor) {
    var method = descriptor.value;
    descriptor.value = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        console.log.apply(console, __spreadArray(["calling ".concat(String(propertyName), ": ")], args, false));
        return method.apply(this, args);
    };
}
var Calculator = /** @class */ (function () {
    function Calculator() {
    }
    Calculator.prototype.add = function (a, b) {
        return a + b;
    };
    Calculator.prototype.minus = function (a, b) {
        return a - b;
    };
    __decorate([
        Log
    ], Calculator.prototype, "add", null);
    __decorate([
        Log
    ], Calculator.prototype, "minus", null);
    return Calculator;
}());
//# sourceMappingURL=index.js.map

重点来了!!!

其中和装饰器有关的包括2部分

  • add方法被装饰器包裹为__decorate([Log], Calculator.prototype, "add", null),对装饰器Log传入了类的原型Calculator.prototype和方法名称add
  • 同时补充了 __decorate方法,我们一句一句来分析
Typescript 复制代码
// 入参定义
// decorators 我们自己写的装饰器函数
// target 类 或者 类的原型
// key 被装饰的成员名
// desc 属性描述符
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    
    // 如果入参数量小于3,说明是类装饰器,r = target 保存类的构造函数
    // 否则,如果desc是空(属性描述符传入的为空),则通过Object.getOwnPropertyDescriptor获取属性描述符
    // 如果desc不为空(已经传入了属性描述符),则直接用传入的desc
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    
    // 这里检查一下运行时环境有没有实现Reflect.decorate,如果已经实现了,直接用Reflect.decorate
    // 如果没有Reflect.decorate,才用自己实现的逻辑
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);

    // 这是TS自己实现的装饰器逻辑
    // decorators是装饰器的数组,可能为多个,从后往前执行
    // 如果 c < 3 类装饰器 调用d(r)直接把类的构造函数传禁区
    // 如果 c > 3 方法/访问器装饰器 调用d(target, key, r)把类的原型,被装饰的名称,属性描述符传入
    // 如果 c = 3 属性装饰器 调用d(target,key)把类的原型,被装饰的名称传入
    // 将r更新为装饰器的返回值(如果没有返回值,则不更新,但装饰器的副作用已经执行)
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    
    // 如果 c > 3 方法/访问器装饰器,并且r有值(表示装饰器返回了新的描述符),则通过defineProperty应用描述符,并返回。否则直接返回r
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};

综上,仅仅通过5行的__decorate方法,就实现了应用装饰器的能力。(不得不佩服前辈的水平)

5. 鸿蒙中的装饰器有何不同

鸿蒙中的装饰器从设计上,也是希望开发者通过简单的写法能够实现特定功能的增强 。但是,对于ArkTS自带的装饰器(比如 @State等),鸿蒙底层并非通过 __decorate方法实现,而是直接将对应的装饰器编译为具有特定功能的目标代码。

为什么要这么做?

  • 在TS中,由于每个业务实现的自定义装饰器功能逻辑都不一样,所以TS官方提供了一套通用的__decorate方法来解决此问题,让所有装饰器都能运行。
  • 在鸿蒙ArkTS中,既然鸿蒙官方提供的装饰器都是具备特定功能的,如果直接将装饰器编译为实现对应功能的代码,执行效率肯定比通用的__decorate更高

以@State装饰器(用于定义一个可变状态,当状态变更时,对应的UI会发生变化)为例。假设ArkTS源码如下:

Typescript 复制代码
@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Row() {
      Text(this.message)
    }
  }
}

经过鸿蒙编译过后对应的代码如下:

编译后的代码可在build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/pages/Index.ts中找到

Typescript 复制代码
interface Index_Params {
    message?: string;
}
class Index extends ViewPU {
    constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
        super(parent, __localStorage, elmtId, extraInfo);
        if (typeof paramsLambda === "function") {
            this.paramsGenerator_ = paramsLambda;
        }
        this.__message = new ObservedPropertySimplePU('Hello World', this, "message");
        this.setInitiallyProvidedValue(params);
        this.finalizeConstruction();
    }
    setInitiallyProvidedValue(params: Index_Params) {
        if (params.message !== undefined) {
            this.message = params.message;
        }
    }
    private __message: ObservedPropertySimplePU<string>;
    get message() {
        return  this.__message.get();
    }
    set message(newValue: string) {
        this.__message.set(newValue);
    }
    initialRender() {
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Row.create();
        }, Row);
        this.observeComponentCreation2((elmtId, isInitialRender) => {
            Text.create(this.message);
        }, Text);
        Text.pop();
        Row.pop();
    }
    // ... 省略其他无关内容
}
// ... 省略其他无关内容

可以发现

  • 原先定义的@State message: string = 'Hello World';变成了this.__message = new ObservedPropertySimplePU('Hello World', this, "message");将对应变量用ObservedPropertySimplePU类包裹
  • 原先使用的Text(this.message)变成了this.observeComponentCreation2((elmtId, isInitialRender) => {Text.create(this.message);}, Text);

可以推测出,其中ObservedPropertySimplePU用于监听某个变量发生变化,且this.observeComponentCreation2能够在对应变量发生变化时,触发UI重新渲染。

具体这两个方法是如何做到状态与UI同步的,这里不再分析,感兴趣的同学可自行进行分析。

再往下扒一扒,看看鸿蒙是怎么生成这些代码的

对ets源码编译的逻辑在developtools_ace_ets2bundle仓库中

在生成变量初始化的代码时(createVariableInitStatement),如果遇到了@State或者@Provide装饰器,会使用ObservedPropertySimplePU类。

同时,在这个仓库中,你可以找到大量不同装饰器的代码生成逻辑。

由此可见,鸿蒙针对内置的每个装饰器,都会有不同的代码生成逻辑,来保证生成后代码的运行效率。此处抛砖引玉,感兴趣的同学可对代码生成细节进一步分析。

∞. 小结

简单总结一下

  • 装饰器是一种特殊类型的声明,能够用简洁的语法实现功能增强。

  • TS以及众多前端开源项目中,都有装饰器的身影。

  • TS底层,装饰器会被编译成 __decorate调用,来实现自定义装饰器。

  • 但是在ArkTS中,鸿蒙的构建工具会对内置的装饰器进行特定的代码生成,来保证最终的执行效率。

如果你对装饰器还有更多的疑问,欢迎留言,我们一起讨论,共同进步~

相关推荐
沈剑心5 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
Georgewu5 小时前
【HarmonyOS】鸿蒙应用加载读取csv文件
前端·harmonyos
Georgewu6 小时前
【HarmonyOS】 鸿蒙图片或视频保存相册
前端·harmonyos
川石教育11 小时前
鸿蒙开发-ArkTS 中使用 filter 组件
harmonyos·鸿蒙·鸿蒙应用开发·鸿蒙开发·鸿蒙开发培训·arkts语言
李洋-蛟龙腾飞公司12 小时前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据权限与基础数据
分布式·华为·harmonyos
Damon小智12 小时前
HarmonyOS NEXT 技术实践-实现音乐服务卡片
华为·harmonyos·鸿蒙·harmonyos next·服务卡片
play_big_knife12 小时前
鸿蒙项目云捐助第十七讲云捐助我的页面上半部分的实现
华为·harmonyos·鸿蒙·云开发·鸿蒙开发·鸿蒙next·华为云开发
枫叶丹418 小时前
【HarmonyOS之旅】HarmonyOS开发基础知识(三)
华为od·华为·华为云·harmonyos
SoraLuna1 天前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
AORO_BEIDOU1 天前
单北斗+鸿蒙系统+国产芯片,遨游防爆手机自主可控“三保险”
华为·智能手机·harmonyos