【前端】浅析元数据与Typescript装饰器

·  阅读 1342

开始之前期待已对下面api有一定认知:

Overview

元数据:实际上就是一组特别的常量,通常储存着有关class的各种信息,在运行时可读取这些信息做一些操作,目前主要由 reflect-metadata 库提供实现。

Typescript装饰器:基本语法为 @xxx 放置在类的各个不同地方。实际上即Typescript会编译出一段特别的代码,这段代码会调用你的xxx函数,并传入class的相关信息作为参数。

元编程:这里不太准确的描述一下,当你把元数据与装饰器语法一起使用时,就可以称作为元编程(代码逻辑和平常都有点不太一样了呢)

一个的demo(官方Readme):

class C {
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");

let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method");
复制代码

reflect-metadata 库

简介

本质是一个可以单独运作的库,通过IIFE(立即调用函数表达式)形式,往全局的Reflect对象增加了一些方法。并且与typescript有特别的配合,当启用typescript emitDecoratorMetadata 选项时,typescript会在生成的装饰器函数调用中额外添加一些代码(调用 reflect-metadata 相关api)

实现分析

下面会以大家熟悉的写法展示,源码的某些变量名有些鬼畜

储存结构

内部维护一个全局变量

const Metadata = new WeakMap<object, Map<string | symbol, Map<any, any>>>()
复制代码

一个“三维map”的结构,基本上所有的api就是对这个map进行取值和存值

API:defineMetadata

除去源代码中各种边界判断,核心思想如下:

function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
  let targetMetadata = Metadata.get(target);
  if (!targetMetadata) {
    targetMetadata = new Map();
    Metadata.set(target, targetMetadata);
  }
  let metadataMap = targetMetadata.get(propertyKey);
  if (!metadataMap) {
    metadataMap = new Map();
    targetMetadata.set(propertyKey, metadataMap);
  }
  metadataMap.set(metadataKey, metadataValue);
}
复制代码

根据targetpropertyKey 两个参数,把metadataKeymetadataValue存入“第三维度”的Map中,该Map即这个target的“元数据”。 注意,该api中 propertyKey 并不是必传参数(它的typescript声明了重载),但这套逻辑依然无问题,因为 Map 是支持undefined 作为它的key值的。

Object has a new [[Metadata]] internal property that will contain a Map whose keys are property keys (or undefined) and whose values are Maps of metadata keys to metadata values.

API:getMetadata

这里 省略 很多边界判断和继承猜测,核心思想如下:

function getMetadata(metadataKey, target, propertyKey) {
  const targetMetadata = Metadata.get(target);
  if (targetMetadata) {
    return targetMetadata.get(propertyKey)?.get(metadataKey);
  }
  const parent = Object.getPrototypeOf(target);
  return getMetadata(metadataKey, parent, propertyKey);
}
复制代码

主要逻辑就是从全局 Metadata 找回对应的值并返回。如果 target 找不到,会尝试从 target原型链上“找”。

这个“找”字就很灵魂了,主要是根据typescript对ES5、ES6类的处理和浏览器做一些兼容性判断,使得“类继承”也可以通过原型链找到父类的元数据。

API:hasOwnMetadata、getOwnMetadata、getOwnMetadataKeys、deleteMetadata

这几个api就简单了,单词中含有“own”代表在查找元数据的过程中,不通过原型链去查找,仅查找自己有的。前三个实际分别对应 map.hasmap.getmap.keys 这三个实例方法。

至于 deleteMetadata ,很明显也只会删除自己有的元数据(毕竟js的delete操作符也不会偷偷通过原型链删除原型的属性) ,对应map.delete 实例方法。

把这些对应的实例方法套进去 getMetadata 的实现,再移除原型链判断基本就是官方实现了。详情此略

API:getMetadataKeys

返回当前对象及其原型链上能够获得的所有元数据的keys,结果去重。去重的优先级同原型链查找顺序,当前对象的元数据keys>原型对象的元数据keys。删除了各种边界判断也就10几行代码,此略

API:metadata

封装 defineMetadata,返回一个符合typescript要求的装饰器函数。下面代码移除了边界判断:

function metadata(metadataKey, metadataValue) {
  function decorator(target, propertyKey) {
    Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
  }
  return decorator;
}
复制代码

Typescript装饰器

这里就不涉及typescript的内部生成策略了(没看😀),简单看看生成出来的代码是个什么样子。

首先明确:@xxx 语法除了“构造函数头上”,能够放在类的任意成员上(包括函数的参数、构造函数的参数)。对于不同的位置,typescript有不同的生成处理策略。另外这种语法一般称之为“注解”(java过来的名词)

实现分析

例子

这部分直接涉及 类、成员函数、成员属性的装饰器,以及辅助函数代码段。感觉不太好拆开来讲解,都在这里一块阐述了。

源代码
import 'reflect-metadata'
function f() {
    console.log("f(): evaluated");
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): called");
    }
}

@f() // ts这里会报错
class C {
    @f()
    method(a: number, b: string) {}

    @f() // ts这里会报错
    p: string = "123";
}
复制代码
编译结果
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);
};
import 'reflect-metadata';
function f() {
    console.log("f(): evaluated");
    return function (target, propertyKey, descriptor) {
        console.log("f(): called");
    };
}
let C = class C {
    constructor() {
        this.p = "123";
    }
    method(a, b) { }
};
__decorate([
    f(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number, String]),
    __metadata("design:returntype", void 0)
], C.prototype, "method", null);
__decorate([
    f() // ts这里会报错
    ,
    __metadata("design:type", String)
], C.prototype, "p", void 0);
C = __decorate([
    f() // ts这里会报错
], C);
复制代码

可以看到在生成了 class C 后,typescript还生成了一段代码,让我们开始仔细分析。

类的字段信息传递

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
// ...
__decorate([
    f(),
    __metadata("design:type", Function), // 仅当开启emitDecoratorMetadata选项时,typescript才生成metadata相关代码
    __metadata("design:paramtypes", [Number, String]),
    __metadata("design:returntype", void 0)
], C.prototype, "method", null);
__decorate([
    f(),
    __metadata("design:type", String)
], C.prototype, "p", void 0);
C = __decorate([
    f()
], C);
复制代码

__metadata 简单封装了 Reflect.metadata,详情参考上面的分析。

对于注解语法,直接把@xxx中的xxx 搬到 __decorate 的第一个参数的数组中。

对于__decorate ,它会接受2个/4个参数:第一个是装饰器函数的数组,第二个是target,第三个是key,第四个是null 或者 undefined

这段代码中,typescript把一些类的信息抽了出来,如类里面的函数名、函数参数对应的构造函数、函数的返回类型、成员属性的键名。因此你可以在运行时,通过上述的Reflect API,把这些信息拿到。

__decorate 函数

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;
};
复制代码

这几行需要慢慢看,毕竟是编译产物,写法比较糟糕。简单介绍一下它的做法:

  1. 根据参数个数(2个/4个)判断当前处理函数是作用于 类 or 类的成员函数/成员属性
  2. 如果第四个参数是null(即作用于成员函数上),那么根据 targetkey 获取属性描述符。该属性描述符后续会传递到装饰器函数中
  3. 判断当前是否引用了 reflect-metadata 库,如果是就把装饰器数组的处理交给它执行
  4. 如果没有引用 reflect-metadata 库,那么直接开始处理(实际区别不大)。倒叙开始遍历传入的装饰器数组,根据参数个数判断如何调用装饰器函数。如果这个装饰器函数调用后有返回值,那么把这个值当作下一次循环的参数 (这里其实我有点没懂,参数个数为3是什么情况,它生成的代码的参数不是2个就是4个)
  5. 最后如果不是作用于类上而且描述符 r 存在,那么给它重新设置回去;如果是作用于类,把最后一个装饰器返回的值r 返回

上面的讲解建议作辅助用途,想精通还得慢慢把这几行代码啃一下。很多行为和边界处理只有看代码才能体会到。

对于类的静态成员函数/静态成员属性

雷同,除了target不同,静态成员函数/静态成员属性的target会是 C本身,而不是C.prototype。如果你对类很熟悉,应该能理解这样做的区别。详情略。

对于参数上的注释

稍微改动下:

class C {
    @f()
    method(@f() a: number, @f() b: string) {}
}
复制代码

有变化的编译结果:

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};

__decorate([
    f(),
    __param(0, f()), __param(1, f()),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number, String]),
    __metadata("design:returntype", void 0)
], C.prototype, "method", null);
复制代码

会多了一个 __param的辅助函数,然后在对应的那个函数的__decorate调用中作为装饰器调用。主要目的就是通过闭包手段,把 paramIndex 作为传给装饰器的第三个参数。

原因是显然易见的,对于参数,装饰器处理阶段它没有所谓的属性描述符,没有意义。但是这个参数在函数中是“第几个参数”这个信息才是有用的(比如你可以通过Reflect.getMetaData("design:paramtypes", target, key)[index]来获取这个参数对应的构造函数)

注解位置顺序与对象

由上可见,对于不同的注解位置,调用__decorate 进行处理的顺序是不同的:成员函数->成员属性->静态成员函数->静态成员属性->类。参数的注解跟随其对应的函数,且调用顺序在函数默认的几个装饰器函数之后,在自定义装饰器函数之前。

另外注意,由于类的本质也是原型链:

  1. 对于 成员函数/成员属性,target 会是 C.prototypekey 就是自己在类中的名字
  2. 对于 静态成员函数/静态成员属性,target 会是 Ckey 就是自己在类中的名字
  3. 对于类,target 就是自己
  4. 对于函数的参数(包括构造函数,有兴趣自己上typescript写个demo看看),target会是C.prototypeC(取决于它是哪个函数的参数),key是对应的那个函数的函数名(如果是构造函数就没有)

注意点

在装饰器中,对于成员属性/静态成员属性,p: string = "1"p = "1" 是不同的。前者是 __metadata("design:type", String) ,后者会是__metadata("design:type", Object),如果这对你很重要(通常是你使用的框架会有问题),务必显式声明。

类似的,函数中剩余参数不会别识别为数组。如method(a: number, ...b: string[]) {}method(a: number, b: string[]) {},前者是__metadata("design:paramtypes", [Number, String]),后者是__metadata("design:paramtypes", [Number, Array])

最后

又没抽到雷律,没得玩,只能水一水文章,哭了😂

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改