Node.jsの登場により、JavaScriptはフロントエンドとバックエンドの両方で共通の言語になりました。ただし、Node.jsの助けを借りてフロントエンドフィールドに登場したAngular、React、Vueなどの多くの優れたエンジニアリングフレームワークとは異なり、バックエンドのExpressやKoaなどの有名なツールフィールドは重要な問題を解決することができませんでした-アーキテクチャ。Nestが登場したのはこのような状況で、Angularのデザインアイデアに深く影響を受けており、Angularのパターンの多くはJavaのSpringフレームワークに由来しているため、NestはNode.jsのSpringフレームワークであると言えます。
したがって、多くのJavaバックエンドの学生にとって、Nestの設計とその記述方法は非常に理解しやすいですが、フロントエンドの従来のJSプログラマーにとっては、反転など、Nestの最も重要でコアなアイデアのみが言及されています。制御の反転、依存性注入などの概念は禁止されています。もちろん、その原則にはTypeScript、デコレータ、メタデータ、リフレクションなどの関連概念も含まれます。さらに、公式ドキュメントとコアコミュニティは英語であるため、多くの学生が参加します。ブロックされました。ドアの外。
Nest.jsの入門シリーズの記事は、Nestの設計アイデアから始まり、関連する概念と原則を詳細に説明し、最後に非常に単純な(または単純な)FakeNestフレームワークを模倣して実装します。Nestの原理をすでに使用していて、もっと学びたいと思っている学生は、何かを得ることができます。また、従来のJSフロントエンド開発に従事している学生が開始できるようにすることもできます。バックエンド開発から借用したいくつかの優れたアイデアを学びます。
この記事は、Nest.jsの使用を開始する第2部であり、Nest.jsの実装のコア構文であるデコレータとメタデータについて詳しく説明します。TSデコレータ構文の使用とメタデータの概念の説明から始めて、デコレータとメタデータの間で衝突する火花を徐々に調査し、最後にデコレータの背後にある実装原理を深く掘り下げて、1つの記事でデコレータとメタデータを理解します。
まず、デコレータの最初の味
デコレータは、クラス宣言とメンバーにアノテーションを追加する方法を提供します。Javascriptのデコレータは、現在、提案募集の第2フェーズにありますが、TypeScriptの実験的な機能としてサポートされています。
注:デコレータは実験的な機能であり、将来のリリースで変更される可能性があります。
デコレータは、クラス宣言、メソッド、アクセサ、プロパティ、またはパラメータにアタッチできる特殊なタイプの宣言です。デコレータは@expressionの形式を使用します。式が評価された後、それは関数である必要があります。式が評価された後、パラメータとして関数に渡されます。
ことわざにあるように、百ものものを見ることはそれらを見ることよりも優れています。デコレータの概念を繰り返すのではなく、それを感じるためにデコレータを直接記述します。
// 类装饰器使用!!!这里被装饰者是类Example
@classDecor
class Example{
// 这里为了this.text不报错,声明了所有属性都为合法属性
[x: string]: any
print(){
console.log(this.text)
}
}
// 类装饰器声明!!!可以看到被装饰者的信息作为参数传入了,这里类装饰器的参数是被装饰类的构造函数
function classDecor(constructor: Function){
console.log('ClassDecor is called')
constructor.prototype.text = 'Class is decorated'
}
console.log('New Example instance')
new Example().print()
// 输出什么?Bingo!
// ClassDecor is called
// New Example instance
// Class is decorated
复制代码
ここでは、classDecorという名前の関数を定義します。この関数は、@記号で装飾され、典型的なクラスデコレータとしてExampleクラスの前に配置されます。コードの最後で、新しいExampleを呼び出してExampleインスタンスを生成し、そのインスタンスでprintメソッドを呼び出します。classDecorクラスデコレータが存在するため、Exampleクラスで定義されていないtext属性がインスタンス化された印刷でアクセスされ、その値が正常に印刷されていることがわかりますClass is decorated
。さらに、クラスデコレータはプログラムの最初の実行時に呼び出されるため、印刷さClassDecor is called
れるNew Example instance
前に印刷されます。このため、Exampleインスタンスにtext属性をマウントすることはできません(classDecorは、次の場合はまだ存在しません。 classDecor)。instance)を実行します。代わりに、プロトタイプチェーンにマウントします。
もちろん、TSはクラスデコレータに加えて、メソッドデコレータ、アクセサデコレータ、プロパティデコレータ、およびパラメータデコレータも提供します。この記事を長くしすぎないようにするために、ここではこれらのデコレータの詳細な使用法については説明しません。しかし、それらが何であるかを簡単に説明するために、それらをすべて組み合わせて、デコレータで装飾されたクラスを作成して見てみましょう!(注:これらのデコレータは、定義および使用方法を説明するためだけに、実際の意味はありません)
// 类装饰器
@classDecor
class Example{
// 属性装饰器
@attributeDecor
attribute: string;
// 访问器装饰器
@accessorDecor
get accessor(): string{
return this.attribute
}
// 方法装饰器
@functionDecor
// 参数装饰器
func(@paramsDecor params: number): number{
return params
}
}
function classDecor(constructor: Function){
//constructor类的构造函数
console.log('classDecor is called')
}
function functionDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
// target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey成员的名字
// descriptor成员的属性描述符
console.log('functionDecor is called')
}
function accessorDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
// target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey成员的名字
// descriptor成员的属性描述符
console.log('accessorDecor is called')
}
function attributeDecor(target: any, propertyKey: string){
// target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey成员的名字
console.log('attributeDecor is called')
}
function paramsDecor(target: Object, propertyKey: string | symbol, parameterIndex: number){
// target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
// propertyKey成员的名字
// parameterIndex参数在函数参数列表中的索引
console.log('paramsDecor is called')
}
console.log('new Example instance')
new Example()
// attributeDecor is called
// accessorDecor is called
// paramsDecor is called
// functionDecor is called
// classDecor is called
// new Example instance
复制代码
次の記事をさらに読む前に、これらのデコレータの使用方法とそのパラメータについて一般的に理解しておくことをお勧めします。デコレータを初めて使用する場合は、TS公式Webサイト( www.tslang.cn/docs/handbo…)のデコレータの章を確認してください。
2.メタデータとリフレクション
デコレータがどのように実装されているかを理解するには、メタデータという概念も導入する必要があります。
元数据是用来描述数据的数据(Data that describes other data)。 —— 阮一峰(元数据(MetaData))
在TS中,我们通常使用reflect-metadata来支持元数据相关的API。目前该库还不是ECMAScript (JavaScript)标准的一部分。不过在未来,随着装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。
TS本身虽然支持定义数据的类型等信息,但这些信息只存在于提供给TS编译器用作编译期执行静态类型检查。经过编译后的代码会成为无类型的传统JS代码。为了能够使JS具备运行时获取数据类型、代码状态、自定义内容等信息,reflect-metadata给我们提供了一系列相关方法,下面一段代码展示了其基础的使用方法。
// 还未成为标准,因此想使用reflect-metadata中的方法就需要手动引入该库,引入后相关方法会自动挂在Reflect全局对象上
import 'reflect-metadata'
class Example {
text: string
}
// 定义一个exp接收Example实例,: Example/: string提供给TS编译器进行静态类型检查,不过这些类型信息会在编译后消失
const exp: Example = new Example()
// 注意:手动添加元数据仅为展示reflect-metadata的使用方式,实际上大部分情况下应该由编译器在编译时自动添加相关代码
// 为了在运行时也能获取exp的类型,我们手动调用defineMetadata方法为exp添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'Example', exp)
// 为了在运行时也能获取text属性的类型,我们手动调用defineMetadata方法为exp的属性text添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'String', exp, 'text')
// 运行时调用getMetadata方法,传入希望获取的元数据key以及目标就可以得到相关信息(这里得到了exp以及text的类型信息)
// 输出'Example' 'String'
console.log(Reflect.getMetadata('type', exp))
console.log(Reflect.getMetadata('type', exp, 'text'))
复制代码
除了defineMetadata(定义元数据)、getMetadata(获取元数据)这两个最基础的方法外,reflect-metadata还提供了hasMetadata(判断元数据是否存在)、hasOwnMetadata(判断元数据是否存在非原型链上)、getOwnMetadata(获取非原型链上元数据)、getMetadataKeys(枚举存在的元数据)、getOwnMetadataKeys(枚举存在非原型链上的元数据)、deleteMetadata(删除元数据)以及@Reflect.metadata装饰器(定义元数据)这一系列元数据操作方法。
有同学可能在这里会出现一个疑问,我们为什么需要在运行时获取数据类型等元数据信息呢?诚然,这些信息对于我们所要实现的业务一般而言并没有什么意义,不过它是实现Javascript反射机制的基础!
在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
反射机制对于依赖注入、运行时类型断言、测试是非常有用的。虽然JavaScript可以通过 Object.getOwnPropertyDescriptor() 或 Object.keys()等函数获取一些实例上的信息,然而我们还是需要反射来实现更加强大的功能,因此TS通过引入reflect-metadata并通过其操作元数据实现了反射机制。
反射机制是Nest.js能够遵守依赖倒置原则解藕依赖,实现控制反转和依赖注入的基础。(不理解这些概念的同学可以回到该系列的第一篇Nest.js入门 —— 控制反转与依赖注入(一)了解相关概念)不过,这里还没有到深入研究Nest.js是如何使用这种机制来完成其核心代码设计的时候。
三、装饰器与元数据
TS在编译过程中会去掉原始数据类型相关的信息,将TS文件转换为传统的JS文件以供JS引擎执行。但是,一旦我们引入reflect-metadata并使用装饰器语法对一个类或其上的方法、属性、访问器或方法参数进行了装饰,那么TS在编译后就会自动为我们所装饰的对象增加一些类型相关的元数据,目前只存在以下三个键:
- 类型元数据使用元数据键"design:type"
- 参数类型元数据使用元数据键"design:paramtypes"
- 返回值类型元数据使用元数据键"design:returntype"
这几个键会根据装饰器类型的不同而被自动添加不同的值,下面让我们将将其放入Example类之中,看看它们分别会返回什么。
@classDecor
class Example{
@attributeDecor
attribute: string;
@accessorDecor
get accessor(): string{
return this.attribute
}
constructor(attribute: string){
this.attribute = attribute
}
@functionDecor
func(@paramsDecor params: number): number{
return params
}
}
function classDecor(constructor: Function){
// 输出 classDecor undefined [ [Function: String] ] undefined
console.log('classDecor')
console.log(Reflect.getMetadata('design:type', constructor))
console.log(Reflect.getMetadata('design:paramtypes', constructor))
console.log(Reflect.getMetadata('design:returntype', constructor))
}
function functionDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
// 输出 functionDecor [Function: Function] [ [Function: Number] ] [Function: Number]
console.log('functionDecor')
console.log(Reflect.getMetadata('design:type', target, propertyKey ))
console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function accessorDecor(target: any, propertyKey: string, descriptor: PropertyDescriptor){
// 输出 accessorDecor [Function: String] [] undefined
console.log('accessorDecor')
console.log(Reflect.getMetadata('design:type', target, propertyKey ))
console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function attributeDecor(target: any, propertyKey: string){
// 输出 attributeDecor [Function: String] undefined undefined
console.log('attributeDecor')
console.log(Reflect.getMetadata('design:type', target, propertyKey ))
console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
function paramsDecor(target: Object, propertyKey: string | symbol, parameterIndex: number){
// 输出 paramsDecor [Function: Function] [ [Function: Number] ] [Function: Number]
console.log('paramsDecor')
console.log(Reflect.getMetadata('design:type', target, propertyKey ))
console.log(Reflect.getMetadata('design:paramtypes', target, propertyKey))
console.log(Reflect.getMetadata('design:returntype', target, propertyKey))
}
复制代码
可以看到,对于不同类型的装饰器,不同的类型相关元数据被自动赋予了相关值,这些值可以在后续的程序运行过程中被拿取并发挥不同的作用。为了读者方便理解其作用,这里我们举一个简单的参数校验例子来展示其强大的作用。
下方代码中的validate是一个方法装饰器,它会在运行时校验其所装饰的任意方法中的入参,并将其与我们定义的TS类型一一比较并查看两者类型是否一致。你可能会怀疑为何要在运行时校验,毕竟TS已经提供了类型的静态校验。但是很快你会看到,一些使用者并不一定会像我们预期的一样调用我们的方法,实际上到处乱写any的TS编程者并不在少数!这会使得TS静态校验形同虚设。因此,运行时校验在某些情况下是必须的!
为了尽可能地缩减代码长度,简化代码结构,validate仅支持基础类型参数的校验,并且也不考虑参数缺省的情况。
class Example {
// 方法装饰器,标明要对print方法的入参做校验
@validate
print(val: string){
console.log(val)
}
}
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 得到所装饰方法上的所有参数类型
const paramstypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
const originFunc = descriptor.value
// 通过描述符修改所装饰方法,在运行原始方法前先调用_innervalidate来动态检查入参类型
descriptor.value = function(...args: any[]){
_innervalidate(args, paramstypes)
originFunc.apply(this, args)
}
function _innervalidate(args: any[], types: any[]) {
// 入参与所需参数长度不相同,报错!(不考虑参数缺省情况)
if(args.length != types.length){
throw new Error('Error: Wrong params length')
}
// 依次比对入参与所需参数的类型并判断它们是否相等(这里只校验了基本类型)
args.forEach((arg, index)=>{
const type = types[index].name.toLowerCase()
if((typeof arg) != type){
throw new Error(`Error: Need a ${type}, but get the ${typeof arg} ${arg} instead!`)
}
})
}
}
const example = new Example()
// val1符合预期,但val2在这里骗过了TS编译器
const val1:any = 'test'
const val2:any = 23
// 尝试运行
try{
// 通过校验,打印 'test'
example.print(val1)
// 报错!'Error: Need a string, but get the number 23 instead!'
// 没有骗过我们的validate装饰器,因为我们在运行时动态获取了它的类型!
example.print(val2)
}catch(e) {
console.log(e.message)
}
复制代码
可以看到,通过使用装饰器与元数据,我们可以做到一些以前无法想象的事情!
四、装饰器实现
现在,让我们再深入一些,看一下TS装饰器语法是如何实现的。我们通过tsc指令让TS帮助我们编译第一节中的代码,打开编译后的JS文件,就可以看到如下代码。这里为了清晰起见展示将全部代码都粘贴在了这里,不过不用急于一行行去阅读,后面我将会把它们一一分解展示并详细描述各部分的功能。
"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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
require("reflect-metadata");
let Example = class Example {
get accessor() {
return this.attribute;
}
func(params) {
return params;
}
};
__decorate([
attributeDecor,
__metadata("design:type", String)
], Example.prototype, "attribute", void 0);
__decorate([
accessorDecor,
__metadata("design:type", String),
__metadata("design:paramtypes", [])
], Example.prototype, "accessor", null);
__decorate([
functionDecor,
__param(0, paramsDecor),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], Example.prototype, "func", null);
Example = __decorate([
classDecor
], Example);
function classDecor(constructor) {
console.log('classDecor is called');
}
function functionDecor(target, propertyKey, descriptor) {
console.log('functionDecor is called');
}
function accessorDecor(target, propertyKey, descriptor) {
console.log('accessorDecor is called');
}
function attributeDecor(target, propertyKey) {
console.log('attributeDecor is called');
}
function paramsDecor(target, propertyKey, parameterIndex) {
console.log('paramsDecor is called');
}
console.log('new Example instance');
const example = new Example();
复制代码
我们先从__metadata
函数看起,这个函数可以简单的理解为等价于Reflect.metadata
。
细看这个函数,它可以被分解为两个部分:
-
先判断当前环境中也就是
this
中是否存在已定义过的__metadata
:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收2个参数的函数; -
函数中判断Reflect.metadata是否存在并为函数(还记得我们说过reflect-metadata并非ECMAScript标准的一部分,需要手动引入吗?):如果该方法存在,那么直接调用它并将传入的k/v参数做为元数据的key和value,并返回一个函数;若不存在,则__metadata方法就为一个空函数,什么都不做!
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
复制代码
接下来,我们看一下__param
函数做了什么,这是一个典型的高阶函数(柯里化)!
-
先判断当前环境中也就是
this
中是否存在已定义过的__param
:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收2个参数的函数; -
返回一个接收2个值的函数,这个函数执行时会将第一步中所得到的
decorator
自定义参数装饰器作为方法,将paramIndex
所装饰参数的序号、target
所装饰类的构造函数或类的原型对象、key
所装饰类方法的名字作为参数调用。
可以看到,它的实际作用就是为了在未来调用用户自定义的参数装饰器并将其所需参数一一传入,实际上该函数就是这就是参数装饰器的实现,这个最简单的装饰器并没有向所装饰类添加任何元数据。
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
复制代码
现在,在深入研究__decorate
函数细节之前,我们先看一下主流程中__decorate
这个函数是如何被调用的。
- 属性装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,添加
key: 'design:type' | value: String
元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为undefined
(void 0
就是undefined
,之所以使用void 0
是因为undefined
可以被用户赋值改变,不安全)。 - 访问器装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,添加
key: 'design:type' | value: String
元数据函数,添加key: 'design:paramtypes' | value: []
元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为null
。 - 方法装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,参数装饰器_param函数,添加
key: 'design:type' | value: String
元数据函数,添加key: 'design:paramtypes' | value: []
元数据函数,添加key: 'design:returntype' | value: undefied
元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为null
。 - 类装饰器中我们传入2个参数。其中第1个参数为函数数组[自定义装饰器函数];第2个参数为类的构造函数。注意在类装饰器中
__decorate
函数的返回值会覆盖所装饰的类,这给予了我们使用类装饰器修改一个类的能力。
这里我把装饰器的种类及其所传入的参数个数,还有第4个参数加粗了,这些信息在之后的__decorate
函数实现中会被用到!
// 属性装饰器
__decorate([
attributeDecor,
__metadata("design:type", String)
], Example.prototype, "attribute", void 0);
// 访问器装饰器
__decorate([
accessorDecor,
__metadata("design:type", String),
__metadata("design:paramtypes", [])
], Example.prototype, "accessor", null);
// 方法装饰器
__decorate([
functionDecor,
// 参数装饰器
__param(0, paramsDecor),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", void 0)
], Example.prototype, "func", null);
// 类装饰器
Example = __decorate([
classDecor
], Example)
复制代码
最后,我们看一下__decorate
这个函数。
-
先判断当前环境中也就是
this
中是否存在已定义过的__decorate
:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收4个参数的函数,分别是decorators
函数数组、target
所装饰类的构造函数或类的原型对象、key
所装饰类成员的名字、desc
所装饰类成员的描述符; -
定义一个变量
c
存储运行时实际传入__decorate
函数的参数个数; -
定义一个变量
r
,r
中存储的内容根据实际传入__decorate
函数的参数个数不同而不同:a. 传入2个时,
r
为类的构造函数或类的原型对象;b. 传入4个时,
r
根据desc
是否为null
,是则存储类成员的描述符(访问器装饰器、方法装饰器),否则为undefined
(属性装饰器 ); -
定义一个未初始化变量
d
; -
判断是否存在
Reflect.decorate
,若存在,则直接调用该方法。这里我们不深入Reflect.decorate
进行研究,它的作用与下方的else这行一致。之所以进行这个判断是因为TS希望Reflect.decorate
在未来能够成为ES的标准,到时候这些旧代码不用更改就可以与新的标准兼容; -
这一步是该函数的核心。从后向前遍历
decorators
装饰器函数数组,并在每次遍历中将遍历到的函数赋值给变量d
。若d不为空,则根据运行时实际传入__decorate
函数的参数个数进入不同的分支:a. 传入2个时(<3, 类装饰器),将
r
中存储的类的构造函数或类的原型对象做为唯一参数传入d
中并调用。b. 传入4个时(>3,属性装饰器、访问器装饰器、方法装饰器 ),将
target
类的构造函数或类的原型对象、key
装饰的类成员的名字以及r
类成员的描述符或undefined
传入d
中并调用。c. 传入3个时(目前并不存在这种情况,猜测可能是属性装饰器的兼容实现),将
target
类的构造函数或类的原型对象、key
装饰的类成员的名字传入d
中并调用。最后,重新赋值
r
为函数d
运行后的返回值(若有)或者r
本身(d
无返回值); -
若实际传入
__decorate
函数的参数为4个且r
存在,那我们将装饰器所装饰目标的值替换为r
; -
返回
r
。
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;
};
复制代码
到这为止,我们就将装饰器的实现全部讲完了。(这里没有涉及到静态方法、参数、访问器的装饰器实现,它们与非静态基本一致!试着想想它们是怎样调用__decorate
函数的,然后自己验证一下!)
通过装饰器的实现可以清晰的看到,装饰器主要做了以下两件事情:
- 根据装饰器类型,将不同的元数据(
design:type
、design:paramtypes
、design:returntype
)添加到被装饰目标上; - 调用装饰器方法,并根据装饰器类型传入其所需的参数。
这里还有几个细节值得注意:
decorators
装饰器函数数组是倒序遍历并调用,因此装饰器的执行顺序是从靠近被装饰者的装饰器开始依次向上执行;- 装饰器函数执行后中若存在返回值,则返回值会通过
Object.defineProperty(target, key, r)
替代被装饰者的值; - 不要试图将访问器装饰器或方法装饰器中拿到的desc描述符对象重新赋值,这样并不会生效,取而代之我们可以尝试改变desc描述符对象上的某几项属性;
- 装饰器会紧跟在类声明后执行,并非实例化后执行;
- 不同类型的装饰器执行顺序依次是——属性装饰器、访问器装饰器、参数装饰器、方法装饰器、类装饰器。这与我们在第一节中代码运行后所打印的顺序相同!
五、总结
装饰器和元数据对于大多数前端同学来说是非常陌生的,但是熟练的使用它们能够使得我们的代码更上一层楼,从而轻松完成一些之前难以解决的任务。学习它们的使用及原理是大有裨益的。
回到本系列的主要任务,学习了解装饰器及元数据的使用及其实现是理解Nest.js源码的基础。通过结合控制反转、依赖注入的思想以及装饰器、元数据的使用,Nest.js的核心设计就能够轻松的完成。下一篇中,我们将分析Nest.js的主要设计思想并模仿实现一个简化版本的FakeNest框架。