2023-07-14

TypeScript 中的元数据以及 reflect-metadata 实现原理分析

本文主要介绍 TypeScript 常搭配使用的reflect-metadata是什么;如何使用reflect-metadata来操作元数据;解读reflect-metadata的实现原理以及规范。

reflect-metadata 是一个 JavaScript 库,用于在运行时访问和操作装饰器的元数据。它提供了一组 API,可以读取和写入装饰器相关的元数据信息。元数据是关于代码中实体(例如类、方法、属性等)的描述性信息。它可以包含有关实体的类型、特性、配置选项、附加信息等。元数据可以在运行时被访问和使用,以便进行进一步的处理、验证、配置等操作。元数据与装饰器密切相关,因为装饰器可以用来添加或读取元数据。装饰器本身是一种特殊类型的声明,可以附加到类、方法、属性或参数上,以修改它们的行为或添加额外的元数据。(装饰器相关的内容详见:)

什么是元数据?

元数据(Metadata)是一种描述性信息,用于描述和解释代码中的数据或结构。定义一个数组来存放数据,那么数组的length属性就是数组的元数据。定义一个类来表达特殊的数据结构,该类的类型就是类的元数据。通常我们可以通过设计时给对应的类、属性、方法、参数等设置元数据,用来标记注解或解释代码。元数据的定义、访问和修改通常使用 reflect-metadata 来实现。 TypeScript 中的装饰器经常用来定义元数据, TypeScript 在编译的时候会执行装饰器函数代码并将元数据附加到对应的目标上【类、属性、方法、函数参数等等】。

元数据通常用于以下场景上:

  • 装饰器(decorator),在 TypeScript 中可以使用元数据来辅助修改和扩展类、方法、属性等行为
  • 依赖注入(DI)。元数据用于标记类的构造函数参数或属性,以便依赖注入容器在运行时自动解析和注入依赖项。
  • ORM 对象关系映射(object relational mapping)。使用元数据来映射数据库表和类之间的关系,以及字段和属性之间的映射关系。
  • 序列化和反序列化。在处理数据的存储和传输时,元数据可以用于制定数据对象的序列化和反序列化规则。

总的来说,元数据是一种描述性信息,可以提供关于代码结构、类型、注解、依赖关系等更多的信息,从而使代码可以更加灵活和可扩展。

在 TypeScript 中通常借助 Reflect-metadata 来解决提供元数据的处理能力。

reflect-metadata 有啥用处?

relect-metadata 是用于对元数据进行定义、修改、查询的一组API,其基于Reflect 对象进行扩展提供一系列API 用于元编程。经典用于解决控制反转比如 DI 依赖注入,这种方式在大型项目和框架开发中使用的比较多,比如:VSCODE编辑器、国产IDE Opensumi、Next.js 框架等等。

github源码库上解释的目标

  • 为(组合设计模式、依赖注入设计模式、运行时类型断言、反射/镜像、测试)提供统一的添加处理元数据的能力
  • 降低开发生成元数据的装饰器的开发难度
  • 扩展元数据的应用范围,不限于在对象上使用,扩展支持其他支持Proxy场景的使用。

Opensumi 中依赖注入框架定义在 opensumi/di 这个库下,感兴趣的可以研究一下。

[图片上传失败...(image-8f35e5-1689346714066)]

Next 框架解决依赖注入的库,封装在 vercel/tsyringe 下。感兴趣的翻翻。

[图片上传失败...(image-6e5cc0-1689346714066)]

如何使用 reflect-metadata

reflect-metadata 对元数据的操作包含几个部分【定义元数据、删除、读取、检查判断(检查分两个方面其一是检查元数据是自己的还是祖上的原型链上的;其二检查一下是否存在对应的元数据)】。定义和读取是比较重要的(先让自行车能骑起来),其他API我们放到实现原理以及规范里面进行介绍。

import "reflect-metadata";

// 定义两个symbol 类型的 metadataKey
const ParamsTypeMetaKey = Symbol("design:paramtypes");
const ReturnTypeMetaKey = Symbol("design:returntype");

// Design-time type annotations, 注意区别使用的字符串作为 metadataKey
function Type(type) {
  return Reflect.metadata("design:type", type);
}

function ParamTypes(...types) {
  return Reflect.metadata(ParamsTypeMetaKey, types);
}
function ReturnType(type) {
  return Reflect.metadata(ReturnTypeMetaKey, type);
}

// 定义一个数据解析的方式元数据
function ParseMethod(type) {
  return Reflect.metadata("data:parse", type);
}

class P {}

export class Point {
  private x: number;
  private y: number;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  @ReturnType("[number, number]")
  getCoord() {
    return [this.x, this.y];
  }

  // 采用装饰器的方式进行元数据定义,另一种方式是显示定义(稍后会说)
  @ParamTypes(Number)
  @ReturnType(P)
  @ParseMethod("JSON")
  moveX(x: number) {
    this.x = x;
    return this;
  }
}

const p = new Point(1, 1);

// 通过metadataKey 从 target 上读取对应属性名的元数据。这里的属性是一个函数
const des = Reflect.getMetadata(ReturnTypeMetaKey, p, "getCoord");
console.log("type is ", des); // type is  [number, number]

const moveXReturnType = Reflect.getMetadata(ReturnTypeMetaKey, p, "moveX");
console.log("moveXReturnType is: ", moveXReturnType); // moveXReturnType is: ƒ P() {}
const moveXParamsType = Reflect.getMetadata(ParamsTypeMetaKey, p, "moveX");
console.log("moveXParamsType is: ", moveXParamsType); //moveXParamsType is: (1) [ƒ Number()]
const moveXParseMethod = Reflect.getMetadata("data:parse", p, "moveX");
console.log("moveXParseMethod is: ", moveXParseMethod); // moveXParseMethod is:  JSON

需要注意的是 Reflect.metadata(metadataKey, metadataValue) API 返回的是一个装饰器函数,该装饰器函数会在 TypeScript 编译的时候自动执行, 并为对应的属性上定义元数据内容:以 metadataKey作为key、以metadataValue 作为 value 。

不了解TypeScript装饰器小伙伴,可以看看上一篇文章# TypeScript 中装饰器 decorator

reflect-metadata 存储元数据的方式是在对应的 target 对象上内建一个[[metadata]] 的属性,该属性对应的值是一个 WeakMap 数据类型的值,元数据内容就存在这个 Map 对象上,metadataKey 作为 WeakMap 的key, metadataValue 作为WeakMap对应的value值。

需要保证 metadataKey 、metadataValue 都需要是符合 ECMAScript 标准的数据类型【Function、Number、String,Object,Symbol等等】,需要注意的是 TS 提供的一些扩展声明比如 type interface等定义出来的类型或接口不能用于 metadataKey 和 metadataValue。值得区分的一点,ES提供的数据内容代码执行的时候是运行时访问的,TS 提供非ES的语法都是需要编译转换的(装饰器这些都是编译时执行)。

接下来介绍一下 reflect-metadata 的实现原理以及规范提供的API。

reflect-metadata 实现原理以及规范

reflect-metadata 是基于 JavaScript 提供的 Reflect 对象进行的扩展和封装(提供一些更丰富的api来操作元数据),metadata 数据存储使用的是 WeakMap 数据结构。 reflect-metadata 代码其实很短,也就几百行。https://github.com/rbuckton/reflect-metadata/blob/master/Reflect.js#L50

对需要反射的对象注册一个内建的属性 [[metadata]] 来存储元数据,reflect-metadata 提供一系列通用的API来实现对这个数据的定义、修改、删除、以及获取。

元数据的定义

需要给 target 定义元数据,需要保证 target 是一个对象类型 IsObject(target) === true。定义的方式有两种:借助装饰器来定义元数据、运行时显示的声明定义元数据。

// 源码

// 检测target的方式
function IsObject(x) {
    return typeof x === "object" ? x !== null : typeof x === "function";
}

借助装饰器来定义元数据

// 案例代码

import "reflect-metadata";

// 定义两个symbol 类型的 metadataKey
const ParamsTypeMetaKey = Symbol("design:paramtypes");

// 返回值是一个装饰器函数
function ParamTypes(...types) {
  return Reflect.metadata(ParamsTypeMetaKey, types);
}

export class Point {
  private x: number;
  private y: number;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // 采用装饰器的方式进行元数据定义,另一种方式是显示定义(稍后会说)
  @ParamTypes(Number)
  moveX(x: number) {
    this.x = x;
    return this;
  }
}

Reflect.metadata(metadataKey, metadataValue) 是一个闭包函数,返回一个装饰器函数,装饰器函数在编译执行的时候会自动拿到需要装饰的对象和属性,以及metadataKey、metadataValue。然后由 OrdinaryDefineOwnMetadata 函数执行元数据的定义。源码如下:

// 源码
function metadata(metadataKey, metadataValue) {
    function decorator(target, propertyKey) {
        if (!IsObject(target))
            throw new TypeError();
        if (!IsUndefined(propertyKey) && !IsPropertyKey(propertyKey))
            throw new TypeError();
        // 定义target 元数据
        OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
    }
    return decorator;
}

运行时主动定义元数据

import "reflect-metadata";

export class Point {
  private x: number;
  private y: number;
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  moveX(x: number) {
    this.x = x;
    return this;
  }
}

// 运行时添加元数据
Reflect.defineMetadata("data:parse", "JSON-1", Point.prototype, "moveX");

const p = new Point(1, 1);

const moveXParseMethod = Reflect.getMetadata("data:parse", p, "moveX");
console.log("moveXParseMethod is: ", moveXParseMethod); // moveXParseMethod is:  JSON-1

使用 Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey) 在代码运行时为 target 添加元数据。这里指的运行时解释两句:js 代码模块在加载的时候会执行模块中的代码,TS 定义的一些内容会在编译时运行并通过编译写入一些 JS 代码到模块文件中,要注意区分这一点。

// 源码实现
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
    if (!IsObject(target))
        throw new TypeError();
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    return OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}

查询元数据

reflect-metadata 提供两种查询方式4个API一种是根据 metadataKey 查询metadataValue。另一种是查询 target 上的 metadataKey 。

import "reflect-metadata";

interface User {
  say(msg: string): string;
}

function TypeMeta(type) {
  return Reflect.metadata("class:type", type);
}

function ReturnType(type) {
  return Reflect.metadata("return:type", type);
}

class Student implements User {
  @TypeMeta("function")
  say(name: string) {
    return `${name} say: hi i am student!`;
  }
}

@TypeMeta("Class")
class Senior extends Student {
  private name: string;
  private age: number;
  constructor(name: string, age: number) {
    super();
    this.name = name;
    this.age = age;
  }

  // @TypeMeta("function")
  @ReturnType("Sneior-string")
  say() {
    return `${this.name} say: hi i am Senior student!`;
  }

  @ReturnType(Number)
  getAge() {
    return this.age;
  }
}

const person = new Senior("tony", 11);

// 为 person 定义上自己的元数据,定义在 class 内部的都是原型链上的,不属于 person 对象,属于 person.prototype 对象
Reflect.defineMetadata("return:type", "person-method", person, "say");

const classType = Reflect.getMetadata("class:type", person, "say");
console.log("classType", classType); // classType function

const ownClassType = Reflect.getOwnMetadata("class:type", person, "say");
console.log("ownClassType", ownClassType); // ownClassType undefined

const ownReturnType = Reflect.getOwnMetadata("return:type", person, "say");
console.log("ownReturnType", ownReturnType); // ownReturnType person-method

const allMetaKeys = Reflect.getMetadataKeys(person, "say");
console.log("getMetadataKeys", allMetaKeys); // getMetadataKeys (2) ["return:type", "class:type"]

const allOwnMetaKeys = Reflect.getOwnMetadataKeys(person, "say");
console.log("allOwnMetaKeys", allOwnMetaKeys); // allOwnMetaKeys (1) ["return:type"]

需要注意的是我们在 Senior 类上定义的元数据对于 person 来说是不是自己的元数据而是他祖先的元数据也就是 person.prototype 上的。查询提供的两组API分别是:

// API参数格式

// get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// get all own metadata keys of an object or property
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

如果不传入属性名称,那么查询的是 person 对象自己的元数据。

// 源码
function getMetadata(metadataKey, target, propertyKey) {
    if (!IsObject(target))
        throw new TypeError();
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    return OrdinaryGetMetadata(metadataKey, target, propertyKey);
}

function getOwnMetadata(metadataKey, target, propertyKey) {
    if (!IsObject(target))
        throw new TypeError();
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    // 区别在这里
    return OrdinaryGetOwnMetadata(metadataKey, target, propertyKey);
}

getMetaxxx 与 getOwnMetaxxx 类API的区别在于,前者在 target 自身的 metadata 上查询不到对应数据会沿着原型链进行查询直到找到对应的数据或者找到最顶层对象返回。

function OrdinaryGetMetadata(MetadataKey, O, P) {
    var hasOwn = OrdinaryHasOwnMetadata(MetadataKey, O, P);
    if (hasOwn)
        return OrdinaryGetOwnMetadata(MetadataKey, O, P);

    // 在上一层原型链上进行查询
    var parent = OrdinaryGetPrototypeOf(O);
    if (!IsNull(parent))
        return OrdinaryGetMetadata(MetadataKey, parent, P);
    return undefined;
}

function OrdinaryGetOwnMetadata(MetadataKey, O, P) {
    var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
    if (IsUndefined(metadataMap))
        return undefined;
    return metadataMap.get(MetadataKey);
}

另一组获取 metadataKeys 的API也是类似的原理,就不赘述,差异在于读取数据的不同,一个读取的值,一个读取的key。

删除元数据

删除元数据就一个 API ,执行的逻辑就是找到 metadata 的Map对象执行 Map上的delete操作。返回值是一个 boolean类型,true表示删除操作执行成功,false表示删除失败或者不存在要删除的元数据。

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

API 比较简单我们就不写demo代码了,直接看看源码实现

// 源码

function deleteMetadata(metadataKey, target, propertyKey) {
    if (!IsObject(target))
        throw new TypeError();
    if (!IsUndefined(propertyKey))
        propertyKey = ToPropertyKey(propertyKey);
    var metadataMap = GetOrCreateMetadataMap(target, propertyKey, /*Create*/ false);
    if (IsUndefined(metadataMap))
        return false;
    if (!metadataMap.delete(metadataKey))
        return false;
    if (metadataMap.size > 0)
        return true;
    var targetMetadata = Metadata.get(target);
    targetMetadata.delete(propertyKey);
    if (targetMetadata.size > 0)
        return true;
    Metadata.delete(target);
    return true;
}

检查判断

元数据的检查只有一组API 分别是 Reflect.hasMetadata、Reflect.hasOwnMetadata 。 功能是查询 target 上是否存在 metadataKey 为键的元数据【注意不关心值是否是真值】。返回值是 boolean 类型, true 表示找到对应的元数据,false 表示不存在对应的元数据。查询关注的是 metadataKey 是否 在 target 的[[metadata]].keys 中存在。

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// check for presence of an own metadata key of an object or property
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
// 源码

function OrdinaryHasOwnMetadata(MetadataKey, O, P) {
    var metadataMap = GetOrCreateMetadataMap(O, P, /*Create*/ false);
    if (IsUndefined(metadataMap))
        return false;
    return ToBoolean(metadataMap.has(MetadataKey));
}

至此 reflect-metadata 相关内容就介绍完了。

参考资料

https://github.com/rbuckton/reflect-metadata/blob/master/Reflect.js#L518

https://github.com/rbuckton/reflect-metadata/tree/master

https://rbuckton.github.io/reflect-metadata/#syntax

TypeScript 中装饰器 decorator https://juejin.cn/post/7254830852068606010

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容