开始之前期待已对下面api有一定认知:
- WeakMap : key为对象(函数),value为任意值的
Map,特性为其保存的引用不影响js垃圾回收 - 属性描述符 : 即 Object.getOwnPropertyDescriptor()
- javascript 的原型链和
class - Typescript装饰器:www.tslang.cn/docs/handbo…
Overview
元数据:实际上就是一组特别的常量,通常储存着有关class的各种信息,在运行时可读取这些信息做一些操作,目前主要由 reflect-metadata 库提供实现。
Typescript装饰器:基本语法为 @xxx 放置在类的各个不同地方。实际上即Typescript会编译出一段特别的代码,这段代码会调用你的xxx函数,并传入class的相关信息作为参数。
元编程:这里不太准确的描述一下,当你把元数据与装饰器语法一起使用时,就可以称作为元编程(代码逻辑和平常都有点不太一样了呢)
一个的demo(官方Readme):
class C {
@Reflect.metadata(metadataKey, metadataValue)
method() {
}
}
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method");
reflect-metadata 库
简介
本质是一个可以单独运作的库,通过IIFE(立即调用函数表达式)形式,往全局的Reflect对象增加了一些方法。并且与typescript有特别的配合,当启用typescript emitDecoratorMetadata 选项时,typescript会在生成的装饰器函数调用中额外添加一些代码(调用 reflect-metadata 相关api)
实现分析
下面会以大家熟悉的写法展示,源码的某些变量名有些鬼畜
储存结构
内部维护一个全局变量
const Metadata = new WeakMap<object, Map<string | symbol, Map<any, any>>>()
一个“三维map”的结构,基本上所有的api就是对这个map进行取值和存值
API:defineMetadata
除去源代码中各种边界判断,核心思想如下:
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
let targetMetadata = Metadata.get(target);
if (!targetMetadata) {
targetMetadata = new Map();
Metadata.set(target, targetMetadata);
}
let metadataMap = targetMetadata.get(propertyKey);
if (!metadataMap) {
metadataMap = new Map();
targetMetadata.set(propertyKey, metadataMap);
}
metadataMap.set(metadataKey, metadataValue);
}
根据target、propertyKey 两个参数,把metadataKey 和 metadataValue存入“第三维度”的Map中,该Map即这个target的“元数据”。 注意,该api中 propertyKey 并不是必传参数(它的typescript声明了重载),但这套逻辑依然无问题,因为 Map 是支持undefined 作为它的key值的。
Object has a new [[Metadata]] internal property that will contain a Map whose keys are property keys (or undefined) and whose values are Maps of metadata keys to metadata values.
API:getMetadata
这里 省略 很多边界判断和继承猜测,核心思想如下:
function getMetadata(metadataKey, target, propertyKey) {
const targetMetadata = Metadata.get(target);
if (targetMetadata) {
return targetMetadata.get(propertyKey)?.get(metadataKey);
}
const parent = Object.getPrototypeOf(target);
return getMetadata(metadataKey, parent, propertyKey);
}
主要逻辑就是从全局 Metadata 找回对应的值并返回。如果 target 找不到,会尝试从 target 的原型链上“找”。
这个“找”字就很灵魂了,主要是根据typescript对ES5、ES6类的处理和浏览器做一些兼容性判断,使得“类继承”也可以通过原型链找到父类的元数据。
API:hasOwnMetadata、getOwnMetadata、getOwnMetadataKeys、deleteMetadata
这几个api就简单了,单词中含有“own”代表在查找元数据的过程中,不通过原型链去查找,仅查找自己有的。前三个实际分别对应 map.has 、map.get、 map.keys 这三个实例方法。
至于 deleteMetadata ,很明显也只会删除自己有的元数据(毕竟js的delete操作符也不会偷偷通过原型链删除原型的属性) ,对应map.delete 实例方法。
把这些对应的实例方法套进去 getMetadata 的实现,再移除原型链判断基本就是官方实现了。详情此略
API:getMetadataKeys
返回当前对象及其原型链上能够获得的所有元数据的keys,结果去重。去重的优先级同原型链查找顺序,当前对象的元数据keys>原型对象的元数据keys。删除了各种边界判断也就10几行代码,此略
API:metadata
封装 defineMetadata,返回一个符合typescript要求的装饰器函数。下面代码移除了边界判断:
function metadata(metadataKey, metadataValue) {
function decorator(target, propertyKey) {
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
}
return decorator;
}
Typescript装饰器
这里就不涉及typescript的内部生成策略了(没看😀),简单看看生成出来的代码是个什么样子。
首先明确:@xxx 语法除了“构造函数头上”,能够放在类的任意成员上(包括函数的参数、构造函数的参数)。对于不同的位置,typescript有不同的生成处理策略。另外这种语法一般称之为“注解”(java过来的名词)
实现分析
例子
这部分直接涉及 类、成员函数、成员属性的装饰器,以及辅助函数代码段。感觉不太好拆开来讲解,都在这里一块阐述了。
源代码
import 'reflect-metadata'
function f() {
console.log("f(): evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
@f() // ts这里会报错
class C {
@f()
method(a: number, b: string) {}
@f() // ts这里会报错
p: string = "123";
}
编译结果
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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import 'reflect-metadata';
function f() {
console.log("f(): evaluated");
return function (target, propertyKey, descriptor) {
console.log("f(): called");
};
}
let C = class C {
constructor() {
this.p = "123";
}
method(a, b) { }
};
__decorate([
f(),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, String]),
__metadata("design:returntype", void 0)
], C.prototype, "method", null);
__decorate([
f() // ts这里会报错
,
__metadata("design:type", String)
], C.prototype, "p", void 0);
C = __decorate([
f() // ts这里会报错
], C);
可以看到在生成了 class C 后,typescript还生成了一段代码,让我们开始仔细分析。
类的字段信息传递
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
// ...
__decorate([
f(),
__metadata("design:type", Function), // 仅当开启emitDecoratorMetadata选项时,typescript才生成metadata相关代码
__metadata("design:paramtypes", [Number, String]),
__metadata("design:returntype", void 0)
], C.prototype, "method", null);
__decorate([
f(),
__metadata("design:type", String)
], C.prototype, "p", void 0);
C = __decorate([
f()
], C);
__metadata 简单封装了 Reflect.metadata,详情参考上面的分析。
对于注解语法,直接把@xxx中的xxx 搬到 __decorate 的第一个参数的数组中。
对于__decorate ,它会接受2个/4个参数:第一个是装饰器函数的数组,第二个是target,第三个是key,第四个是null 或者 undefined。
这段代码中,typescript把一些类的信息抽了出来,如类里面的函数名、函数参数对应的构造函数、函数的返回类型、成员属性的键名。因此你可以在运行时,通过上述的Reflect API,把这些信息拿到。
__decorate 函数
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;
};
这几行需要慢慢看,毕竟是编译产物,写法比较糟糕。简单介绍一下它的做法:
- 根据参数个数(2个/4个)判断当前处理函数是作用于 类 or 类的成员函数/成员属性
- 如果第四个参数是null(即作用于成员函数上),那么根据
target和key获取属性描述符。该属性描述符后续会传递到装饰器函数中 - 判断当前是否引用了
reflect-metadata库,如果是就把装饰器数组的处理交给它执行 - 如果没有引用
reflect-metadata库,那么直接开始处理(实际区别不大)。倒叙开始遍历传入的装饰器数组,根据参数个数判断如何调用装饰器函数。如果这个装饰器函数调用后有返回值,那么把这个值当作下一次循环的参数 (这里其实我有点没懂,参数个数为3是什么情况,它生成的代码的参数不是2个就是4个) - 最后如果不是作用于类上而且描述符
r存在,那么给它重新设置回去;如果是作用于类,把最后一个装饰器返回的值r返回
上面的讲解建议作辅助用途,想精通还得慢慢把这几行代码啃一下。很多行为和边界处理只有看代码才能体会到。
对于类的静态成员函数/静态成员属性
雷同,除了target不同,静态成员函数/静态成员属性的target会是 C本身,而不是C.prototype。如果你对类很熟悉,应该能理解这样做的区别。详情略。
对于参数上的注释
稍微改动下:
class C {
@f()
method(@f() a: number, @f() b: string) {}
}
有变化的编译结果:
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
__decorate([
f(),
__param(0, f()), __param(1, f()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, String]),
__metadata("design:returntype", void 0)
], C.prototype, "method", null);
会多了一个 __param的辅助函数,然后在对应的那个函数的__decorate调用中作为装饰器调用。主要目的就是通过闭包手段,把 paramIndex 作为传给装饰器的第三个参数。
原因是显然易见的,对于参数,装饰器处理阶段它没有所谓的属性描述符,没有意义。但是这个参数在函数中是“第几个参数”这个信息才是有用的(比如你可以通过Reflect.getMetaData("design:paramtypes", target, key)[index]来获取这个参数对应的构造函数)
注解位置顺序与对象
由上可见,对于不同的注解位置,调用__decorate 进行处理的顺序是不同的:成员函数->成员属性->静态成员函数->静态成员属性->类。参数的注解跟随其对应的函数,且调用顺序在函数默认的几个装饰器函数之后,在自定义装饰器函数之前。
另外注意,由于类的本质也是原型链:
- 对于 成员函数/成员属性,
target会是C.prototype,key就是自己在类中的名字 - 对于 静态成员函数/静态成员属性,
target会是C,key就是自己在类中的名字 - 对于类,
target就是自己 - 对于函数的参数(包括构造函数,有兴趣自己上typescript写个demo看看),
target会是C.prototype或C(取决于它是哪个函数的参数),key是对应的那个函数的函数名(如果是构造函数就没有)
注意点
在装饰器中,对于成员属性/静态成员属性,p: string = "1"和p = "1" 是不同的。前者是 __metadata("design:type", String) ,后者会是__metadata("design:type", Object),如果这对你很重要(通常是你使用的框架会有问题),务必显式声明。
类似的,函数中剩余参数不会别识别为数组。如method(a: number, ...b: string[]) {} 和 method(a: number, b: string[]) {},前者是__metadata("design:paramtypes", [Number, String]),后者是__metadata("design:paramtypes", [Number, Array])
最后
又没抽到雷律,没得玩,只能水一水文章,哭了😂