装饰器如何向函数传参

一、为什么会关注这个

在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获取参数位置,并为其赋值,传到对应实例的执行函数中去执行

相关推荐
Jiaberrr17 分钟前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
everyStudy41 分钟前
JS中判断字符串中是否包含指定字符
开发语言·前端·javascript
城南云小白42 分钟前
web基础+http协议+httpd详细配置
前端·网络协议·http
前端小趴菜、42 分钟前
Web Worker 简单使用
前端
web_learning_3211 小时前
信息收集常用指令
前端·搜索引擎
Ylucius1 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
tabzzz1 小时前
Webpack 概念速通:从入门到掌握构建工具的精髓
前端·webpack
200不是二百1 小时前
Vuex详解
前端·javascript·vue.js
滔滔不绝tao1 小时前
自动化测试常用函数
前端·css·html5
LvManBa1 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习