深入解析TypeScript装饰器

虽然装饰器(Decorator)这个特性已经出来很久,但是说实话在前端项目中我从未使用过这项特性(下个项目用起来 💪)。最近我使用NestJS开发了一个Node应用,NestJS深度使用了TypeScript的装饰器功能,从依赖注入到模块组织再到路由管理,都用上了装饰器。虽然我已经学会怎么使用装饰器,但是对于装饰器的工作原理我还一知半解,于是我的好奇心又开始作怪了🤔。接下来我将通过分析TypeScript生成的JavaScript代码以及 reflect-metadata 源码深入解析TypeScript装饰器。

TypeScript是怎么编译的?

TypeScript装饰器并不是默认开启的,需要开启 experimentalDecorators  配置才能启用装饰器功能。那么让我们写个例子,看看TypeScript做了什么:

function sealed(constructor: Function) {
 Object.seal(constructor);
 Object.seal(constructor.prototype); 
}

function log(target: any, propertyName:string, description: TypedPropertyDescriptor<function> ) {
 let method = description.value;
 description.value = function () {
   console.log('logged', target, propertyName)
   return method.apply(this, arguments);
 }
}

function param(target: Object, propertyKey: string | symbol, parameterIndex: number) {
 console.log('it is', propertyKey);
 console.log(arguments);
}

@sealed
class A {
 beGreat() {}
 @log
 hello(@param who: string): string {
   return who
 }
} 

上面这个例子用到了类装饰器,方法装饰器,以及参数装饰器。我们看看TypeScript是怎么编译这三种类型的装饰器的:

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 __param = (this && this.__param) || function (paramIndex, decorator) {
   return function (target, key) { decorator(target, key, paramIndex); }
};
var A = (function () {
   function A() {
   }
   A.prototype.beGreat = function () { };
   A.prototype.hello = function (who) {
       return who;
   };
   __decorate([
       log,
       __param(0, param)
   ], A.prototype, "hello", null);
   A = __decorate([
       sealed
   ], A);
   return A;
}()); 

很显然,TypeScript通过添加__decorator实现了装饰器的功能。不过这里的装饰器只是对类和方法做简单处理罢了,并没有Java中注解的功能,这里我们需要引入一个库reflect-metadata,这个库为注解代码提供了反射支持,而TypeScript中需要开启 emitDecoratorMetadata 配置(其实不开的话也可以获取注解信息,但是就不能获取方法和类的各种类型信息)。加了这些Buff之后,TypeScript编译代码更复杂了。

var __metadata = (this && this.__metadata) || function (k, v) {
   if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var A = (function () {
   function A() {
   }
   A.prototype.beGreat = function () { };
   A.prototype.hello = function (who) {
       return who;
   };
   A.prototype.test = function (i) { return i; };
   __decorate([
       log,
       __param(0, param),
       __metadata("design:type", Function),
       __metadata("design:paramtypes", [String, Number]),
       __metadata("design:returntype", String)
   ], A.prototype, "hello", null);
   __decorate([
       __param(0, param),
       __metadata("design:type", Function),
       __metadata("design:paramtypes", [Number]),
       __metadata("design:returntype", Number)
   ], A.prototype, "test", null);
   A = __decorate([
       sealed
   ], A);
   return A;
}()); 

不过,现在我们可以获取方法参数信息和返回信息:

const functionParamsType = Reflect.getMetadata('design:paramtypes', A.prototype, 'hello');
const returnType = Reflect.getMetadata('design:returntype', A.prototype, 'hello') 
[ [Function: String], [Function: Number] ]
[Function: String] 

写到这里大家想必已经知道如何通过反射库和装饰器实现对代码的标注了。下面是个方法标注的代码模板(仅供参考):

function annotation (params) {
   return function(target: any, propertyKey: string | symbol, desc: TypedPropertyDescriptor<any>) {
       Reflect.defineMetadata('annotation', params, target, propertyKey);
       return desc;
   }
} 

这里reflect-metadata是十分关键的库,不过其实现的功能也相当简单,无非是对元数据的保存和读取。我们看看他的内部实现。

reflect-metadata是怎么实现的?

下面这张图清晰地展示了reflect-metadata的实现

我想看懂这张图,你肯定就理解了reflect-metadata的实现。