Primeros pasos con Nest.js: decorador de TS y metadatos (2)

Con la aparición de Node.js, JavaScript se ha convertido en un lenguaje común tanto para el front-end como para el back-end. Sin embargo, a diferencia de varios frameworks de ingeniería excelentes como Angular, React, Vue, etc., que han aparecido en el campo front-end con la ayuda de Node.js, las famosas herramientas como Express y Koa en el back-end campo no han sido capaces de resolver un problema importante - Arquitectura. Es en este contexto que surgió Nest. Está profundamente inspirado en las ideas de diseño de Angular, y muchos de los patrones de Angular provienen del framework Spring en Java, por lo que podemos decir que Nest es el framework Spring de Node.js.

Por lo tanto, para muchos estudiantes de back-end de Java, el diseño en Nest y su método de escritura son muy fáciles de entender, pero para los programadores tradicionales de JS del front-end, solo se mencionan las ideas más importantes y centrales de Nest, como la inversión. de control, conceptos como la inyección de dependencia son prohibitivos, sin mencionar que sus principios también involucran conceptos relacionados como TypeScript, decoradores, metadatos, reflexión, etc. Además, sus documentos oficiales y comunidades centrales están en inglés, lo que hace que muchos estudiantes bloqueado fuera de la puerta.

La serie de artículos introductorios de Nest.js comenzará con las ideas de diseño de Nest para explicar sus conceptos y principios relacionados en detalle, y finalmente imitará e implementará un marco FakeNest extremadamente simple (o simple). Por un lado, los estudiantes que ya han usado y quieren aprender más sobre los principios de Nest pueden ganar algo y, por otro lado, también intentan permitir que los estudiantes que están involucrados en el desarrollo front-end tradicional de JS comiencen y aprenda algunas ideas excelentes que se han tomado prestadas del desarrollo de back-end.

Este artículo es la segunda parte de cómo comenzar con Nest.js y describirá en detalle la sintaxis central en la implementación de Nest.js: decoradores y metadatos. A partir del uso de la sintaxis del decorador TS y la explicación de los conceptos de metadatos, exploramos gradualmente las chispas que chocan entre los decoradores y los metadatos, y finalmente profundizamos en el principio de implementación detrás de los decoradores, y lo llevamos a comprender los decoradores y los metadatos en un artículo.

Primero, la primera cata del decorador

Los decoradores nos brindan una forma de agregar anotaciones a las declaraciones y miembros de la clase. Los decoradores en Javascript se encuentran actualmente en la segunda fase de la convocatoria de propuestas, pero se admiten como característica experimental en TypeScript.

Nota: Los decoradores son una característica experimental y pueden cambiar en futuras versiones.

Un decorador es un tipo especial de declaración que se puede adjuntar a una declaración de clase, método, descriptor de acceso, propiedad o parámetro. El decorador usa la forma de @expression. Después de evaluar la expresión, debe ser una función. y ligeramente diferente) se pasará como un parámetro a la función después de evaluar la expresión.

Como dice el refrán, ver cien cosas es mejor que verlas, no repetiremos el concepto de decoradores, sino que escribiremos directamente un decorador para sentirlo.

// 类装饰器使用!!!这里被装饰者是类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
复制代码

Aquí definimos una función llamada classDecor, que se decora con el símbolo @ y se coloca antes de la clase Example como un decorador de clase típico. Al final del código, generamos una instancia de ejemplo llamando al nuevo ejemplo y llamando al método de impresión en él. Se puede ver que debido a la existencia del decorador de clase classDecor, se accede al atributo de texto, que no está definido en la clase Ejemplo, en la impresión instanciada y su valor se imprime con éxito Class is decorated. Además, dado que el decorador de clases se llamará la primera vez que se ejecute el programa, se ClassDecor is calledimprimirá New Example instanceantes de que se imprima, es por esta razón que no podemos montar el atributo de texto en la instancia de Ejemplo (el classDecor aún no existe cuando ejecutando la instancia classDecor), en su lugar lo montamos en su cadena prototipo.

Por supuesto, además de los decoradores de clases, TS también proporciona decoradores de métodos, decoradores de accesorios, decoradores de propiedades y decoradores de parámetros. Para no alargar demasiado este artículo, no entraremos en detalles sobre el uso de estos decoradores aquí. Pero para explicar brevemente qué son, ¡combinémoslos todos y escribamos una clase decorada con decoradores para ver! (Nota: estos decoradores no tienen un significado real, solo para ilustrar cómo deben definirse y usarse)

// 类装饰器
@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
复制代码

Espero que tenga una comprensión general de cómo se utilizan estos decoradores y sus parámetros antes de seguir leyendo los siguientes artículos. Si es nuevo en los decoradores, consulte el capítulo de decoradores en el sitio web oficial de TS ( www.tslang.cn/docs/handbo…

2. Metadatos y reflexión

Para comprender cómo se implementan los decoradores, también debemos introducir un concepto: los metadatos.

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

Supongo que te gusta

Origin juejin.im/post/7086673578858397732
Recomendado
Clasificación