TypeScript 装饰器的基础 Reflect-metadata

320 阅读4分钟

reflect-metadata 是一个为 JavaScript/TypeScript 提供元数据反射 API 的库,主要解决以下问题:

  • 元数据存储与读取:在装饰器模式中,需要为类、方法、属性附加额外的信息(如类型、配置、依赖关系等),但原生 JavaScript 缺乏标准化的元数据存储机制。
  • 框架依赖管理:支持依赖注入(DI)框架(如 Angular、NestJS)自动解析类依赖,通过元数据获取构造函数参数类型。
  • 装饰器增强:使装饰器能够声明式地附加元数据(如路由信息、验证规则),供运行时动态读取。

本文记录 Reflect-metadata 实现原理TypeScript 装饰器源码实现

Reflect-metadata 实现原理

reflect-metadata 的核心目标是为 JavaScript/TypeScript 提供元数据存储和反射能力。其实现原理基于以下关键机制:


1. 元数据存储模型

reflect-metadata 使用 分层存储结构 来管理元数据,核心数据结构是 WeakMap,确保对象被回收时元数据自动释放,避免内存泄漏。

存储层级
  • Metadata Key:元数据的唯一标识(如字符串或 Symbol)。
  • Target Object:元数据关联的目标(类、原型、实例等)。
  • Property Key:可选属性或方法名(如类属性、方法)。

存储结构如下:

// 伪代码表示
const MetadataStorage = new WeakMap<Object, Map<string | Symbol, Map<string | undefined, any>>>();
  • 外层 WeakMap:键为 目标对象(如类构造函数),值为该对象的所有元数据。
  • 中层 Map:键为 元数据键(如 'design:type'),值为具体属性或方法的元数据。
  • 内层 Map:键为 属性名(如 'myMethod'),值为实际存储的元数据值。
存储示例
@Reflect.metadata('key', 'value')
class MyClass {
  @Reflect.metadata('prop', 123)
  myProp: string;
}

存储结构伪代码:

MetadataStorage = {
  [MyClass]: {
    'key': { 
      undefined: 'value' // 类级别的元数据(无属性名)
    },
    'prop': {
      'myProp': 123      // 属性级别的元数据
    }
  }
};

2. 元数据读写流程

2.1 写入元数据

通过 Reflect.defineMetadata 或装饰器 @Reflect.metadata 写入:

const metadataStore = new WeakMap();
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
    let targetMetadata = metadataStore.get(target);
    if (!targetMetadata) {
        targetMetadata = new Map();
        metadataStore.set(target, targetMetadata);
    }
    let propertyMetadata;
    if (propertyKey) {
        propertyMetadata = targetMetadata.get(propertyKey);
        if (!propertyMetadata) {
            propertyMetadata = new Map();
            targetMetadata.set(propertyKey, propertyMetadata);
        }
    } else {
        propertyMetadata = targetMetadata;
    }
    propertyMetadata.set(metadataKey, metadataValue);
}

2.2 读取元数据

通过 Reflect.getMetadata 读取:

function getMetadata(metadataKey, target, propertyKey) {
    const targetMetadata = metadataStore.get(target);
    if (!targetMetadata) {
        return undefined;
    }

    let propertyMetadata;
    if (propertyKey) {
        propertyMetadata = targetMetadata.get(propertyKey);
        if (!propertyMetadata) {
            return undefined;
        }
    } else {
        propertyMetadata = targetMetadata;
    }

    return propertyMetadata.get(metadataKey);
}

3. TypeScript 元数据自动生成

当开启 emitDecoratorMetadata 时,TypeScript 编译器会在 编译阶段 自动为装饰器修饰的目标生成以下元数据:

3.1 生成规则
  • design:type:被装饰目标的类型。
    • 类属性 → 属性类型(如 StringNumber)。
    • 方法 → Function
  • design:paramtypes:方法或构造函数的参数类型数组。
  • design:returntype:方法的返回值类型。
3.2 实现机制

TypeScript 编译器在编译时注入代码,例如:

class MyClass {
  @Decorator
  method(param: string): number {
    return 42;
  }
}

编译后(简化):

// 自动注入类型元数据
Reflect.defineMetadata("design:paramtypes", [String], MyClass.prototype, "method");
Reflect.defineMetadata("design:returntype", Number, MyClass.prototype, "method");

4. 原型链与继承处理

reflect-metadata 支持通过原型链查找元数据,例如:

class Parent {
  @Reflect.metadata('key', 'parent-value')
  method() {}
}

class Child extends Parent {
  method() {}
}

// 读取 Child 的 method 元数据时,会沿原型链查找 Parent
const value = Reflect.getMetadata('key', Child.prototype, 'method'); // 'parent-value'
实现细节
  • 当目标对象不存在元数据时,递归查找其原型链。
  • 使用 Object.getPrototypeOf() 遍历原型链,直到找到元数据或到达 null

5. 性能优化策略

reflect-metadata 通过以下方式优化性能:

  • WeakMap 弱引用:目标对象被销毁时,自动释放其元数据,避免内存泄漏。
  • 惰性初始化:仅在首次写入元数据时创建存储结构,减少内存占用。
  • 原型链缓存:在查找元数据时缓存遍历结果,减少重复计算。

6. 核心 API 实现

Reflect.metadata 装饰器工厂
function metadata(key: any, value: any) {
  return (target: any, propertyKey?: string) => {
    Reflect.defineMetadata(key, value, target, propertyKey);
  };
}
Reflect.getOwnMetadata

直接读取目标自身的元数据,不查找原型链

function getOwnMetadata(key, target, propertyKey) {
  const targetMetadata = MetadataStorage.get(target);
  // ... 仅读取当前目标的元数据
}

7. 边界条件处理

  • 非对象目标:若目标不是对象(如原始类型 number),抛出错误。
  • Symbol 键处理:支持 Symbol 作为元数据键,避免命名冲突。
  • 默认元数据:若未找到元数据,返回 undefined

总结

reflect-metadata 的实现核心在于:

  1. 使用 WeakMap 实现高效、安全的元数据存储。
  2. 与 TypeScript 编译器深度集成,自动生成类型元数据。
  3. 支持原型链查找和继承逻辑。
  4. 通过装饰器模式和反射 API 提供声明式元数据操作。

其设计平衡了灵活性、性能与内存安全,成为现代 JavaScript/TypeScript 生态中元编程的基石。接下来是TypeScript 中装饰器的实现思路以及装饰器代码执行时机。