Nest.js入门 —— TS装饰器与元数据(二)

随着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入门的第二篇,将详细讲述在Nest.js的实现中最核心的语法——装饰器与元数据。我们从TS装饰器语法的使用、元数据概念的讲解出发,逐步发掘装饰器与元数据所碰撞出的火花,最终深入到装饰器背后的实现原理,带你一文读懂装饰器与元数据。

一、装饰器初尝

装饰器为我们在类的声明及成员上添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。

注意:装饰器是一项实验性特性,在未来的版本中可能会发生改变。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。装饰器使用 @expression这种形式,expression求值后必须为一个函数,该函数会在运行时被调用(不管类是否实例化都会第一时间调用),被装饰者的信息(根据被装饰着不同而略有不同)将做为参数传入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类之前做为一个典型的类装饰器使用。在代码的最后,我们通过调用new Example生成一个Example实例并调用其上的print方法。可以看到由于classDecor类装饰器的存在,实例化后的print中访问到了text这个并未在Example类中定义的属性并成功打印了它的值Class is decorated。另外,由于类装饰器会在程序运行的第一时间被调用,因此ClassDecor is called会先于New Example instance被打印出来,也正是因为这个原因我们无法将text属性挂载在Example实例上(运行classDecor时还不存在该实例),取而代之我们将其挂载载了其原型链上。

当然,除了类装饰器之外,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官网装饰器章节(www.tslang.cn/docs/handbo…

二、元数据与反射

要理解装饰器是如何实现的,我们还需要引入一个概念 —— 元数据。

元数据是用来描述数据的数据(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

细看这个函数,它可以被分解为两个部分:

  1. 先判断当前环境中也就是this中是否存在已定义过的__metadata:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收2个参数的函数;

  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函数做了什么,这是一个典型的高阶函数(柯里化)!

  1. 先判断当前环境中也就是this中是否存在已定义过的__param:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收2个参数的函数;

  2. 返回一个接收2个值的函数,这个函数执行时会将第一步中所得到的decorator自定义参数装饰器作为方法,将paramIndex所装饰参数的序号、target所装饰类的构造函数或类的原型对象、key所装饰类方法的名字作为参数调用。

可以看到,它的实际作用就是为了在未来调用用户自定义的参数装饰器并将其所需参数一一传入,实际上该函数就是这就是参数装饰器的实现,这个最简单的装饰器并没有向所装饰类添加任何元数据。

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
复制代码

现在,在深入研究__decorate函数细节之前,我们先看一下主流程中__decorate这个函数是如何被调用的。

  1. 属性装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,添加key: 'design:type' | value: String元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为undefinedvoid 0就是undefined,之所以使用void 0是因为undefined可以被用户赋值改变,不安全)。
  2. 访问器装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,添加key: 'design:type' | value: String元数据函数,添加key: 'design:paramtypes' | value: []元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为null
  3. 方法装饰器中我们传入4个参数。其中第1个参数为函数数组[自定义装饰器函数,参数装饰器_param函数,添加key: 'design:type' | value: String元数据函数,添加key: 'design:paramtypes' | value: []元数据函数,添加key: 'design:returntype' | value: undefied元数据函数];第2个参数为类的原型对象(当装饰static静态属性时应为类的构造函数);第3个参数为所装饰类成员的名字;第4个参数为null
  4. 类装饰器中我们传入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这个函数。

  1. 先判断当前环境中也就是this中是否存在已定义过的__decorate:若该函数已被定义过,则不再重复定义,直接使用之前定义过的;若该函数为未定义,则定义一个能接收4个参数的函数,分别是decorators函数数组、target所装饰类的构造函数或类的原型对象、key所装饰类成员的名字、desc所装饰类成员的描述符;

  2. 定义一个变量c存储运行时实际传入__decorate函数的参数个数;

  3. 定义一个变量rr中存储的内容根据实际传入__decorate函数的参数个数不同而不同:

    a. 传入2个时r为类的构造函数或类的原型对象;

    b. 传入4个时r根据desc是否为null,是则存储类成员的描述符(访问器装饰器方法装饰器),否则为undefined属性装饰器 );

  4. 定义一个未初始化变量d

  5. 判断是否存在Reflect.decorate,若存在,则直接调用该方法。这里我们不深入Reflect.decorate进行研究,它的作用与下方的else这行一致。之所以进行这个判断是因为TS希望Reflect.decorate在未来能够成为ES的标准,到时候这些旧代码不用更改就可以与新的标准兼容;

  6. 这一步是该函数的核心。从后向前遍历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无返回值);

  7. 若实际传入__decorate函数的参数为4个且r存在,那我们将装饰器所装饰目标的值替换为r

  8. 返回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函数的,然后自己验证一下!)

通过装饰器的实现可以清晰的看到,装饰器主要做了以下两件事情:

  1. 根据装饰器类型,将不同的元数据(design:typedesign:paramtypesdesign:returntype)添加到被装饰目标上;
  2. 调用装饰器方法,并根据装饰器类型传入其所需的参数。

这里还有几个细节值得注意:

  1. decorators装饰器函数数组是倒序遍历并调用,因此装饰器的执行顺序是从靠近被装饰者的装饰器开始依次向上执行;
  2. 装饰器函数执行后中若存在返回值,则返回值会通过Object.defineProperty(target, key, r)替代被装饰者的值;
  3. 不要试图将访问器装饰器或方法装饰器中拿到的desc描述符对象重新赋值,这样并不会生效,取而代之我们可以尝试改变desc描述符对象上的某几项属性;
  4. 装饰器会紧跟在类声明后执行,并非实例化后执行;
  5. 不同类型的装饰器执行顺序依次是——属性装饰器、访问器装饰器、参数装饰器、方法装饰器、类装饰器。这与我们在第一节中代码运行后所打印的顺序相同!

五、总结

装饰器和元数据对于大多数前端同学来说是非常陌生的,但是熟练的使用它们能够使得我们的代码更上一层楼,从而轻松完成一些之前难以解决的任务。学习它们的使用及原理是大有裨益的。

回到本系列的主要任务,学习了解装饰器及元数据的使用及其实现是理解Nest.js源码的基础。通过结合控制反转、依赖注入的思想以及装饰器、元数据的使用,Nest.js的核心设计就能够轻松的完成。下一篇中,我们将分析Nest.js的主要设计思想并模仿实现一个简化版本的FakeNest框架。

猜你喜欢

转载自juejin.im/post/7086673578858397732