装饰器如何向函数传参

一、为什么会关注这个

在nestjs中,有大量的装饰器的使用,对于controller中函数可以使用@Query,@Param,@Body等函数装饰来获取相应的请求相关数据

typescript 复制代码
  @Get()
  findAll(@Query('a') a) {
    return this.testService.findAll() + a;
  }

  @Get('/test')
  findTest(@Body() b, @Query('a') a, @Ip() ip: string) {
    console.log(b, a, ip);
    return this.testService.findAll() + a + b + ip;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.testService.findOne(+id);
  $}$

通过装饰器,我们可以获取自己想要的数据,并且不必在乎参数的顺序。 对于装饰器,装饰类,方法,属性我们见到的比较多,对于参数的装饰,一时不知道实现的原理,所以进行了一些探究

二、装饰器的基本介绍

TypeScript 学习笔记 - 装饰器 这篇文章对于装饰器的介绍比较全面,对于各种装饰器及其特性都进行了介绍并举了例子

三、对于参数装饰器注入参数的实现

前置知识:

Reflect.defineMetadata和Reflect.getMetadata

官方文档:github.com/rbuckton/re...

参考文章:juejin.cn/post/684490...

简单来说,我们可以利用Reflect.defineMetadata在类或者对象上设置一个元数据,利用Reflect.getMetadata可以获取出这个数据

实现

1. 定义参数装饰器,利用Reflect.defineMetadata挂载数据

typescript 复制代码
function params1() {
  return (target: any, propertyKey: any, paramsIndex: any) => {
    Reflect.defineMetadata("params1", paramsIndex, target[propertyKey]);
  };
}

function params2() {
  return (target: any, propertyKey: any, paramsIndex: any) => {
    Reflect.defineMetadata("params2", paramsIndex, target[propertyKey]);
  };
}

2.定义运行函数

typescript 复制代码
const run = (instance: any, funcName: string, params1: any, params2: any) => {
  const params1Index = Reflect.getMetadata('params1', instance[funcName]);
  const params2Index = Reflect.getMetadata('params2', instance[funcName]);
  const params: any = [];
  if (params1Index >= 0) {
    params[params1Index] = params1;
  }
  if (params2Index >= 0) {
    params[params2Index] = params2;
  }
  instance[funcName](...params);
}

3. 定义类

typescript 复制代码
class T {
  @addParams('hello', 'world')
  a(@params1() a: any) {
    console.log(a);
  }
  @addParams('hello1', 'world1')
  b(@params2() b: any) {
    console.log(b);
  }
  @addParams('hello2', 'world2')
  c(@params1() a: any, @params2() b: any) {
    console.log(a, b);
  }
  @addParams('hello3', 'world3')
  d(@params2() b: any, @params1() a: any) {
    console.log(b, a);
  }
}

4. 实例化对象

typescript 复制代码
const instance = new T()

5. 利用类装饰器或执行方法执行

typescript 复制代码
run(instance, 'a', 'hello', 'world')

打印结果:

扩展,利用方法装饰器实现

前置知识:方法装饰器和参数装饰器的顺序

方法装饰器求值先于参数装饰器,参数装饰器调用先于方法装饰器,是一个洋葱模型

这里用上面文章中的一个例子

typescript 复制代码
function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

class C {
  @f("Method Outer")
  @f("Method Inner")
  method(
    @f("Parameter Foo Outer") @f("Parameter Foo Inner") foo,
    @f("Parameter Bar") bar
  ) {}
}

输出为:

sql 复制代码
evaluate: Method Outer
evaluate: Method Inner
evaluate: Parameter Foo Outer
evaluate: Parameter Foo Inner
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo Inner
call: Parameter Foo Outer
call: Method Inner
call: Method Outer

定义方法装饰器

typescript 复制代码
function addParams(params1Val: string, params2Val: string){
  return (target: any, propertyKey: string,descriptor: PropertyDescriptor) => {
    const original = descriptor.value;
    const indexA =  Reflect.getMetadata('params1', target[propertyKey])
    const indexB = Reflect.getMetadata('params2', target[propertyKey])
    descriptor.value = function(){
      const args = []
      args[indexA] = params1Val
      args[indexB] = params2Val
      const res = original.call(this, ...args);
      return res
    }
  }
}

这里利用了上述方法和函数装饰器的顺序

执行,直接实例执行

typescript 复制代码
instance.a()

instance.b()

instance.c()

instance.d()

这不是实际使用的方法,因为不能动态的传递参数进入,这里只是介绍方法装饰器和参数装饰器的顺序以及一种设计方法

优化

在nestjs中,参数装饰器数据是是设置在
Reflect.defineMetadata("params1", paramsIndex, target.constructor, propertyKey);

为什么要这么做?

1. defineMetadata第四个参数传不传的问题

在defineMetadata的实现中,

从上述代码可以看出,metadata存储在一个WeakMap中,WeakMap的key是传进去的第三个参数,而我们具体的值,是存储在WeakMap,key为第三个参数的对应的value中,而这个value是一个Map,Map的key为第四个参数,不传则为undefined

所以我们上述的实现,直接将第三个参数传为target[propertyKey]是不正确的,因为这将在WeakMap中,重新设置一个key,而我们希望的是将一个类上的所有metadata都在key为这个类的一个Map中

2. 第三个参数的传值

对于装饰器,传进去的target为类的原型对象,在实际调用的时候,是类的实例,实例和对象其实不是相同的key,按理说应该找不到value,但实际找得到,因为reflect-metadata做了兼容

可以看到,如果找不到,会以原型对象为key继续去找,所以即使我们第三个参数传入target,在使用时传入的获取值的key是实例instance,依旧可以获得,但如果我们手动Reflect.defineMetadata('params1', value, instance)这样就实例在该实例中找到值,就不会去原型对象上面找了

所以我们可以看到,在nestjs中,定义元数据时第三个参数传的是,target.constructor,在使用时是instance.constructor,因为类的原型对象的constructor指向的就是类本身,instance.constructor也就等于 Object.getPrototypeOf(instance).constructor,所以他们指向的是WeakMap中的同一份空间,所以相对比较好的实现是

typescript 复制代码
function params1() {
  return (target: any, propertyKey: any, paramsIndex: any) => {
    Reflect.defineMetadata("params1", paramsIndex, target, propertyKey);
  };
}

function params2() {
  return (target: any, propertyKey: any, paramsIndex: any) => {
    Reflect.defineMetadata("params2", paramsIndex, target, propertyKey]);
  };
}

执行函数

typescript 复制代码
const run = (instance: any, funcName: string) => {
  const params1Index = Reflect.getMetadata('params1', instance, funcName);
  const params2Index = Reflect.getMetadata('params2', instance, funcName);
  const params: any = [];
  if (params1Index >= 0) {
    params[params1Index] = 'hello';
  }
  if (params2Index >= 0) {
    params[params2Index] = 'world';
  }
  instance[funcName](...params);
}

区别仅仅在于第三个和第四个参数的不同,传了第四个参数,这样能够精确的知道到底是类的还是属性或方法的元数据,不然多个函数或属性相同的元数据会使得命名冲突等问题

3. 存储结构

defineMetadata实现源码

四、reflect-metadata的defiendMeta和getMeta原理

1. defiendMeta

typescript 复制代码
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
    // 不是对象报类型错误
    if (!IsObject(target))
        throw new TypeError();
    // 转换一下propertyKey
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    return OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}

// 定义自己的metadata
function OrdinaryDefineOwnMetadata(MetadataKey, MetadataValue, O, P) {
    // 根据O和P,获取到target中propertyKey的metadataMap,此处可参考数据存储图
    var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ true);
    // 将值塞进去
    metadataMap.set(MetadataKey, MetadataValue);
}

var Metadata = new _WeakMap();

function GetOrCreateMetadataMap(O, P, Create) {
    // 获取target的Metadata
    var targetMetadata = Metadata.get(O);
    // 没有就创建
    if (IsUndefined(targetMetadata)) {
        if (!Create)
            return undefined;
        targetMetadata = new _Map();
        Metadata.set(O, targetMetadata);
    }
    // 获取propertyKey的MetadataMap
    var metadataMap = targetMetadata.get(P);
    // 没有就创建
    if (IsUndefined(metadataMap)) {
        if (!Create)
            return undefined;
        metadataMap = new _Map();
        targetMetadata.set(P, metadataMap);
    }
    return metadataMap;
}

2. getMetadata

typescript 复制代码
function getMetadata(metadataKey, target, propertyKey) {
    // target不是对象,报类型错误
    if (!IsObject(target))
        throw new TypeError();
    // 和defined一样的方法转换propertyKey
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    return OrdinaryGetMetadata(metadataKey, target, propertyKey);
}

function OrdinaryGetMetadata(MetadataKey, O, P) {
    // 先判断以target为key是否能找到值
    var hasOwn = OrdinaryHasOwnMetadata(MetadataKey, O, P);
    // 找到了
    if (hasOwn)
    // 返回对应的值
        return OrdinaryGetOwnMetadata(MetadataKey, O, P);
    // 自己target-->propertyKey-->metadataKey这一路找不到值
    var parent = OrdinaryGetPrototypeOf(O);
    // 原型对象不是空
    if (!IsNull(parent))
    // 递归继续往上找
        return OrdinaryGetMetadata(MetadataKey, parent, P);
    return undefined;
}

// 获取自己的metadata
function OrdinaryHasOwnMetadata(MetadataKey, O, P) {
    // 根据O和P,获取到target中propertyKey的metadataMap,此处可参考数据存储图
    var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
    // 自己的propertyKey的metadata为空,就返回找不到
    if (IsUndefined(metadataMap))
        return false;
    // 如果自己的metadata不为空,但是找不到我想找的metadataKey,也返回空,否则就是找到了
    return ToBoolean(metadataMap.has(MetadataKey));
}

function GetOrCreateMetadataMap(O, P, Create) {
    // 获取target的Metadata
    var targetMetadata = Metadata.get(O);
    // 没有就创建
    if (IsUndefined(targetMetadata)) {
        if (!Create)
            return undefined;
        targetMetadata = new _Map();
        Metadata.set(O, targetMetadata);
    }
    // 获取propertyKey的MetadataMap
    var metadataMap = targetMetadata.get(P);
    // 没有就创建
    if (IsUndefined(metadataMap)) {
        if (!Create)
            return undefined;
        metadataMap = new _Map();
        targetMetadata.set(P, metadataMap);
    }
    return metadataMap;
}

function OrdinaryGetOwnMetadata(MetadataKey, O, P) {
    // 再找一遍target的propertyKey的metadataMap
    var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
    if (IsUndefined(metadataMap))
        return undefined;
    // 在对应的metadata里找对应的metadataKey
    return metadataMap.get(MetadataKey);
}

// 找到target的原型对象
function OrdinaryGetPrototypeOf(O) {
    var proto = Object.getPrototypeOf(O);
    if (typeof O !== "function" || O === functionPrototype)
        return proto;
    // TypeScript doesn't set __proto__ in ES5, as it's non-standard.
    // Try to determine the superclass constructor. Compatible implementations
    // must either set __proto__ on a subclass constructor to the superclass constructor,
    // or ensure each class has a valid `constructor` property on its prototype that
    // points back to the constructor.
    // If this is not the same as Function.[[Prototype]], then this is definately inherited.
    // This is the case when in ES6 or when using __proto__ in a compatible browser.
    if (proto !== functionPrototype)
        return proto;
    // If the super prototype is Object.prototype, null, or undefined, then we cannot determine the heritage.
    var prototype = O.prototype;
    var prototypeProto = prototype && Object.getPrototypeOf(prototype);
    if (prototypeProto == null || prototypeProto === Object.prototype)
        return proto;
    // If the constructor was not a function, then we cannot determine the heritage.
    var constructor = prototypeProto.constructor;
    if (typeof constructor !== "function")
        return proto;
    // If we have some kind of self-reference, then we cannot determine the heritage.
    if (constructor === O)
        return proto;
    // we have a pretty good guess at the heritage.
    return constructor;
}

五、总结

这里介绍了如何利用装饰器向类中的函数传参,并且不必关注参数的顺序,以及reflect-metadata的实现原理。在实际中,比如nestjs中实现则更为复杂,但整体的实现原理是Reflect.defineMetadata存储参数位置,Reflect.getMetadata获取参数位置,并为其赋值,传到对应实例的执行函数中去执行

相关推荐
zpjing~.~12 分钟前
CSS 过渡动画效果
前端·css
Senar16 分钟前
机器学习和前端
前端·人工智能·机器学习
GISer_Jing18 分钟前
React基础知识(总结回顾一)
前端·react.js·前端框架
我叫czc1 小时前
【Python高级366】静态Web服务器开发
服务器·前端·python
温轻舟1 小时前
前端开发 -- 自动回复机器人【附完整源码】
前端·javascript·css·机器人·html·交互·温轻舟
赵大仁1 小时前
深入解析 Vue 3 的核心原理
前端·javascript·vue.js·react.js·ecmascript
张小虎在学习1 小时前
JS 数组创建、访问、常用方法
javascript
张小虎在学习1 小时前
JS 三种添加元素的方式、区别( write、createElement、innerHTML )
javascript
csstmg1 小时前
记录一次前端绘画海报的过程及遇到的几个问题
前端
bidepanm1 小时前
Vue.use()和Vue.component()
前端·javascript·vue.js