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中,鸿蒙的构建工具会对内置的装饰器进行特定的代码生成,来保证最终的执行效率。
如果你对装饰器还有更多的疑问,欢迎留言,我们一起讨论,共同进步~