什么是装饰器
- 装饰器是一个函数,这个函数仅在代码加载阶段执行一次。本质就是编译时执行的函数
- 装饰器的语法是 @后跟一个函数或者一个执行后返回函数的表达式
- 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象
- 装饰器有两个版本,一个是2014年通过的,一个是2022年通过的。ArkTS里使用的是2014年通过的
装饰器分类
我们介绍的也只是2014年通过的。如果想了解最新版的装饰器,请看 TypeScript 装饰器
装饰器简单代码示例
@ClassDecorator() // 类装饰器
class A {
@PropertyDecorator() // 属性装饰器
name: string;
@MethodDecorator() // 方法装饰器
fly(
@ParameterDecorator() // 参数装饰器
meters: number
) {
// code
}
@AccessorDecorator() // 存取器装饰器
get egg() {
// code
}
set egg(e) {
// code
}
}
- 注意,构造方法没有方法装饰器,只有参数装饰器。类装饰器其实就是在装饰构造方法。
- 装饰器只能用于类,要么应用于类的整体,要么应用于类的内部成员,不能用于独立的函数
类装饰器(Class Decorators):用于类
类型定义
- 类型参数
TFunction
必须是函数,实际上就是构造方法。 - 类装饰器的返回值,要么是返回处理后的原始构造方法,要么返回一个新的构造方法
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
示例
- 下面定义的Log函数,通过在前面使用@修饰,并将它应用于class A。这样创建A的实例的时候,就会输出log
- 这段代码应用了高级函数的知识。即在Log函数里返回了一个类装饰器函数
function Log(info:string) {
console.log('received: ', info);
return function (target:any) {
console.log('apply decorator');
return target;
}
}
@Log('log something')
class A {}
属性装饰器(Property Decorators):用于属性
属性装饰器不需要返回值,如果有的话,也会被忽略
类型定义
- target:(对于实例属性)类的原型对象(prototype),或者(对于静态属性)类的构造函数。
- propertyKey:所装饰属性的属性名,注意类型有可能是字符串,也有可能是 Symbol 值
type PropertyDecorator =
(
target: Object,
propertyKey: string|symbol
) => void;
示例
装饰器ValidRange
对属性year
设立了一个上下限检查器,只要该属性赋值时,超过了上下限,就会报错
function ValidRange(min:number, max:number) {
return (target:Object, key:string) => {
Object.defineProperty(target, key, {
set: function(v:number) {
if (v < min || v > max) {
throw new Error(`Not allowed value ${v}`);
}
}
});
}
}
// 输出 Installing ValidRange on year
class Student {
@ValidRange(1920, 2020)
year!: number;
}
const stud = new Student();
// 报错 Not allowed value 2022
stud.year = 2022;
方法装饰器(Method Decorators):用于方法
方法装饰器用来装饰类的方法。方法装饰器的返回值(如果有的话),就是修改后的该方法的描述对象,可以覆盖原始方法的描述对象
类型定义
- target:(对于类的静态方法)类的构造函数,或者(对于类的实例方法)类的原型。
- propertyKey:所装饰方法的方法名,类型为
string|symbol
。 - descriptor:所装饰方法的描述对象
type MethodDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
示例
方法装饰器@logger
用来装饰add()
方法,它的作用是让该方法输出日志。每当add()
调用一次,控制台就会打印出参数和运行结果
function logger(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}
class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}
(new C()).add(1, 2)
// params: 1 2
// result: 3
参数装饰器(Parameter Decorators):用于方法的参数
该装饰器不需要返回值,如果有的话会被忽略
类型定义
- target:(对于静态方法)类的构造函数,或者(对于类的实例方法)类的原型对象。
- propertyKey:所装饰的方法的名字,类型为
string|symbol
。 - parameterIndex:当前参数在方法的参数序列的位置(从0开始)
type ParameterDecorator = (
target: Object,
propertyKey: string|symbol,
parameterIndex: number
) => void;
示例
function log(
target: Object,
propertyKey: string|symbol,
parameterIndex: number
) {
console.log(`${String(propertyKey)} NO.${parameterIndex} Parameter`);
}
class C {
member(
@log x:number,
@log y:number
) {
console.log(`member Parameters: ${x} ${y}`);
}
}
const c = new C();
c.member(5, 5);
// member NO.1 Parameter
// member NO.0 Parameter
// member Parameters: 5 5
存取器装饰器(Accessor Decorators):用于类的 set 或 get 方法
存取器装饰器用来装饰类的存取器(accessor)。所谓“存取器”指的是某个属性的取值器(getter)和存值器(setter)
TypeScript 不允许对同一个属性的存取器(getter 和 setter)使用同一个装饰器,也就是说只能装饰两个存取器里面的一个,且必须是排在前面的那一个,否则报错
类型定义
- target:(对于静态属性的存取器)类的构造函数,或者(对于实例属性的存取器)类的原型。
- propertyKey:存取器的属性名。
- descriptor:存取器的属性描述对象
type AccessorDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
示例
function validator(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
){
const originalGet = descriptor.get;
const originalSet = descriptor.set;
if (originalSet) {
descriptor.set = function (val) {
if (val > 100) {
throw new Error(`Invalid value for ${propertyKey}`);
}
originalSet.call(this, val);
};
}
}
class C {
#foo!: number;
@validator
set foo(v) {
this.#foo = v;
}
get foo() {
return this.#foo;
}
}
const c = new C();
c.foo = 150;
// 报错
装饰器的执行顺序
执行装饰器时,按照如下顺序执行。
- 实例相关的装饰器。
- 静态相关的装饰器。
- 构造方法的参数装饰器。
- 类装饰器
- 同一级装饰器的执行顺序,是按照它们的代码顺序。但是,参数装饰器的执行总是早于方法装饰器
- 如果同一个方法或属性有多个装饰器,那么装饰器将顺序加载、逆序执行
- 如果同一个方法有多个参数,那么参数也是顺序加载、逆序执行
@State装饰器探索
@State装饰器只能应用于组件内部,并且它修饰的是属性。从这些特征我们就能知道它是一个属性装饰器
编译成包
我们知道鸿蒙工程是一个多Module的工程,开发态和编译后的包对应视图如下
看一下我们目前的代码工程
进行代码编译成包
查看编译后的结构。可以看到我们编译成了一个.app和一个.hap
解包HAP
右键entry的hap,open in finder
复制一份.hap文件,并修改为.zip后缀,然后双击打开
ets文件夹里面的就是方舟字节码文件,使用010Editor进行解析(很抱歉,我没学会怎么用,借了一张大神的图).看到如下图所示
当我们用@Component修饰一个struct的时候,通过ArkCompiler编译后,其实会生成一个类,这个类继承于ViewPU
arkui_ace_engine源码 查看State
ViewPU定义在ArkTS framework arkui_ace_engine当中,是Openharmony中UI承载的关键类
ViewPU定义
看一下构造函数
/**
* Create a View
*
* 1. option: top level View, specify
* - compilerAssignedUniqueChildId must specify
* - parent=undefined
* - localStorage must provide if @LocalSTorageLink/Prop variables are used
* in this View or descendant Views.
*
* 2. option: not a top level View
* - compilerAssignedUniqueChildId must specify
* - parent must specify
* - localStorage do not specify, will inherit from parent View.
*
*/
constructor(parent: ViewPU, localStorage: LocalStorage, elmtId : number = -1, extraInfo : ExtraInfo = undefined) {
super();
// if set use the elmtId also as the ViewPU object's subscribable id.
// these matching is requiremrnt for updateChildViewById(elmtId) being able to
// find the child ViewPU object by given elmtId
this.id_= elmtId == -1 ? SubscriberManager.MakeId() : elmtId;
this.providedVars_ = parent ? new Map(parent.providedVars_)
: new Map<string, ObservedPropertyAbstractPU<any>>();
this.localStoragebackStore_ = undefined;
stateMgmtConsole.log(`ViewPU constructor: Creating @Component '${this.constructor.name}' from parent '${parent?.constructor.name}}'`);
if (extraInfo) {
this.extraInfo_ = extraInfo;
}
if (parent) {
// this View is not a top-level View
this.setCardId(parent.getCardId());
// Call below will set this.parent_ to parent as well
parent.addChild(this);
} else if (localStorage) {
this.localStorage_ = localStorage;
stateMgmtConsole.debug(`${this.debugInfo()}: constructor: Using LocalStorage instance provided via @Entry.`);
}
SubscriberManager.Add(this);
stateMgmtConsole.debug(`${this.debugInfo()}: constructor: done`);
}
上面的注释写明了,@Component装饰的视图作为顶级视图和非顶级视图的情况
-
顶级视图选项:
- 必须指定
compilerAssignedUniqueChildId
,用于唯一标识子视图。 parent
设置为undefined
,表示这是顶级视图。- 如果在这个视图或其子视图中使用了
@LocalStorageLink/Prop
变量,必须提供localStorage
。
- 必须指定
-
非顶级视图选项:
- 必须指定
compilerAssignedUniqueChildId
,用于唯一标识子视图。 - 必须指定
parent
,指明父级视图。 - 不需要指定
localStorage
,它将从父视图继承。
- 必须指定
看一下最后的代码 它调用SubscriberManager添加自身,后面state的回调会进行callback,同时它也有父子组件的概念,ViewPU有一个parent属性,代表当前的父组件,父子双方共用同一个localStorage,其实就是通过构造函数保证的。
搜索implementation of @State
notifyPropertyHasChangedPU
我们看到属性改变的时候,调用了他所属view的viewPropertyHasChanged