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

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中,鸿蒙的构建工具会对内置的装饰器进行特定的代码生成,来保证最终的执行效率。

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

相关推荐
遇到困难睡大觉哈哈7 小时前
HarmonyOS —— Remote Communication Kit 拦截器(Interceptor)高阶定制能力笔记
笔记·华为·harmonyos
遇到困难睡大觉哈哈8 小时前
HarmonyOS —— Remote Communication Kit 定制处理行为(ProcessingConfiguration)速记笔记
笔记·华为·harmonyos
氤氲息8 小时前
鸿蒙 ArkTs 的WebView如何与JS交互
javascript·交互·harmonyos
遇到困难睡大觉哈哈8 小时前
HarmonyOS支付接入证书准备与生成指南
华为·harmonyos
赵浩生8 小时前
鸿蒙技术干货10:鸿蒙图形渲染基础,Canvas绘图与自定义组件实战
harmonyos
赵浩生9 小时前
鸿蒙技术干货9:deviceInfo 设备信息获取与位置提醒 APP 整合
harmonyos
BlackWolfSky9 小时前
鸿蒙暂未归类知识记录
华为·harmonyos
L、21811 小时前
Flutter 与开源鸿蒙(OpenHarmony):跨平台开发的新未来
flutter·华为·开源·harmonyos
L、21812 小时前
Flutter 与 OpenHarmony 深度融合实践:打造跨生态高性能应用(进阶篇)
javascript·flutter·华为·智能手机·harmonyos