Reflect-Metadata 详解

发布在 Frontend

目录

  1. 引言
  2. Metadata
  3. Reflect Metadata
    1. 概念
    2. 安装/使用
    3. 类/属性/方法 装饰器
    4. 原型链查找
    5. 用途
    6. API
  4. 深入 Reflect Metadata
    1. 实现原理
  5. End
  6. 题外话

引言

在 ES6 的规范当中,就已经存在 Reflect API 了。简单来说这个 API 的作用就是可以实现对变量操作的函数化,也就是反射。具体的关于这个 API 的内容,可以查看这个教程

然而我们在这里讲到的,却是 Reflect 里面还没有的一个规范,那么就是 Reflect Metadata

Metadata

想必对于其他语言的 Coder 来说,比如说 Java 或者 C#,Metadata 是很熟悉的。最简单的莫过于通过反射来获取类属性上面的批注(在 JS 当中,也就是所谓的装饰器)。从而可以更加优雅的对代码进行控制。

而 JS 现在有装饰器,虽然现在还在 Stage2 阶段。但是 JS 的装饰器更多的是存在于对函数或者属性进行一些操作,比如修改他们的值,代理变量,自动绑定 this 等等功能。

所以,后文当中我就使用 TypeScript 来进行讲解,因为 TypeScript 已经完整的实现了装饰器。
虽然 Babel 也可以,但是需要各种配置,人懒,不想配置那么多。

但是却无法实现通过反射来获取究竟有哪些装饰器添加到这个类/方法上。

于是 Reflect Metadata 应运而生。

Reflect Metadata

Relfect Metadata,简单来说,你可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。当然你也可以通过反射来添加这些信息。
就像是下面这个例子所示。

1
2
3
4
5
6
7
8
9
10
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('hello', 'world')
public hello(): string {
return 'hello world'
}
}
Reflect.getMetadata('name', A) // 'A'
Reflect.getMetadata('hello', new A()) // 'world'
// 这里为什么要用 new A(),用 A 不行么?后文会讲到

是不是很简单,那么我简单来介绍一下~

概念

首先,在这里有四个概念要区分一下:

  1. Metadata Key {Any} 后文简写 k。元数据的 Key,对于一个对象来说,他可以有很多元数据,每一个元数据都对应有一个 Key。一个很简单的例子就是说,你可以在一个对象上面设置一个叫做 'name' 的 Key 用来设置他的名字,用一个 'created time' 的 Key 来表示他创建的时间。这个 Key 可以是任意类型。在后面会讲到内部本质就是一个 Map 对象。

  2. Metadata Value {Any} 后文简写 v。元数据的类型,任意类型都行。

  3. Target {Object} 后文简写 o。表示要在这个对象上面添加元数据

  4. Property {String|Symbol} 后文简写 p。用于设置在那个属性上面添加元数据。大家可能会想,这个是干什么用的,不是可以在对象上面添加元数据了么?其实不仅仅可以在对象上面添加元数据,甚至还可以在对象的属性上面添加元数据。其实大家可以这样理解,当你给一个对象定义元数据的时候,相当于你是默认指定了 undefined 作为 Property。下面有一个例子大家可以看一下。

大家明白了上面的概念之后,我之前给的那个例子就很简单了~不用我多说了。

安装/使用

下面不如正题,我们怎么开始使用 Reflect Metadata 呢?
首先,你需要安装 reflect-metadata polyfill,然后引入之后就可以看到在 Reflect 对象下面有很多关于 Metadata 的函数了。因为这个还没有进入正式的协议,所以需要安装垫片使用。

啥,Reflect 是啥,一个全局变量而已。

你不需要担心这个垫片的质量,因为连 Angular 都在使用呢,你怕啥。

之后你就可以安装我上面写的示例,在 TypeScript 当中去跑了。

类/属性/方法 装饰器

看这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const objs = [A, new A, A.prototype]
const res = objs.map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
// 大家猜测一下 res 的值会是多少?

想好了么?再给你 10 秒钟

10
9
8
7
6
5
4
3
2
1

res

1
2
3
4
5
[
['A', undefined, 'A', undefined],
[undefined, 'hello', undefined, undefined],
[undefined, 'hello', undefined, 'hello'],
]

那么我来解释一下为什么回是这样的结果。

首先所有的对类的修饰,都是定义在类这个对象上面的,而所有的对类的属性或者方法的修饰,都是定义在类的原型上面的,并且以属性或者方法的 key 作为 property,这也就是为什么 getMetadata 会产生这样的效果了。

那么带 Own 的又是什么情况呢?

这就要从元数据的查找规则开始讲起了

原型链查找

类似于类的继承,查找元数据的方式也是通过原型链进行的。

就像是上面那个例子,我实例化了一个 new A(),但是我依旧可以找到他原型链上的元数据。

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'
Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'

用途

其实所有的用途都是一个目的,给对象添加额外的信息,但是不影响对象的结构。这一点很重要,当你给对象添加了一个原信息的时候,对象是不会有任何的变化的,不会多 property,也不会有的 property 被修改了。
但是可以衍生出很多其他的用途。

  • Anuglar 中对特殊字段进行修饰 (Input),从而提升代码的可读性。

  • 可以让装饰器拥有真正装饰对象而不改变对象的能力。让对象拥有更多语义上的功能。

API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Reflect {
// 用于装饰器
metadata(k, v): (target, property?) => void
// 在对象上面定义元数据
defineMetadata(k, v, o, p?): void
// 是否存在元数据
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
// 获取元数据
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
// 获取所有元数据的 Key
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
// 删除元数据
deleteMetadata(k, o, p?): boolean
}

大家可能注意到,针对某些操作,会有 Own 的函数。这是因为有的操作是可以通过原型链进行操作的。这个后文讲解。

深入 Reflect Metadata

实现原理

如果你去翻看官网的文档,他会和你说,所有的元数据都是存在于对象下面的 [[Metadata]] 属性下面。一开始我也是这样认为的,新建一个 Symbol('Metadata'),然后将元数据放到这个 Symbol 对应的 Property 当中。直到我看了源码才发现并不是这样。请看例子

1
2
3
@Reflect.metadata('name', 'A')
class A {}
Object.getOwnPropertySymbols(A) // []

哈哈,并没有所谓的 Symbol,那么这些元数据都存在在哪里呢?

其实是内部的一个 WeakMap 中。他正是利用了 WeakMap 不增加引用计数的特点,将对象作为 Key,元数据集合作为 Value,存到 WeakMap 中去。

如果你认真探寻的话,你会发现其内部的数据结构其实是这样的

1
WeakMap<any, Map<any, Map<any, any>>>

是不是超级绕,但是我们从调用的角度来思考,这就一点都不绕了。

1
weakMap.get(o).get(p).get(k)

先根据对象获取,然后在根据属性,最后根据元数据的 Key 获取最终要的数据。

End

因为 Reflect Metadata 实在是比较简单,这里就不多讲解了。更多内容请查看 Spec

题外话

其实看了源码之后还是挺惊讶的,按照一般的套路,很多 polyfill 会让你提供一些前置的 polyfill 之后,当前的 polyfill 才能使用。但是 reflect-metadata 竟然内部自己实现了很多的 polyfill 和算法。比如 Map, Set, WeakMap, UUID。最惊讶的莫过于 WeakMap 了。不是很仔细的阅读了一下,好像还是会增加引用计数。

注释和共享

目录

  1. Golang
    1. Timer
    2. Ticker

这一篇文章,我们主要讲解一下不同语言之间关于定时器的操作和坑。

首先说一些这个坑中的一些前提,首先我这里所有的都是和 JavaScript 与 C++ 相比较进行学习的,所以下面不会出现 JavaScript 相关的教程,除非 JavaScript 在这个上面的坑太多,非讲不可。如果你没有相关的语言基础,请去学习。谢谢。

首先先从 golang 语言讲起来

Golang

在 golang 中单次定时器和循环定时器分别是 Timer 和 Ticker。这两个都是一个结构体,结构如下

1
2
3
4
5
6
7
8
type Timer struct {
       C <-chan Time
       // contains filtered or unexported fields
}
type Ticker struct {
       C <-chan Time // The channel on which the ticks are delivered.
       // contains filtered or unexported fields
}

其中 C 是一个 只读的 Time 类型的 channel,定时器根据传入的时间设置,定时向这个 channel 输入时间。你需要做的是等待这个 channel 中的数据。

每一个结构体都有如下方法:

1
2
3
func (t *Timer) Reset(d Duration) bool
func (t *Timer) Stop() bool
func (t *Ticker) Stop()

首先 Timer 和 Ticker 的区别不是很大,下面我们将两者的共性以 Timer 来讲述, 后面会针对两者不同的内容进行讲解。

1
2
3
4
5
6
7
8
9
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second * 2)
time := <- timer.C
   println("Timer expired")
}

参数接受一个时间参数,表示多久后向 channel 传输数据,也就是你想要定时的间隔,上面这个例子表示两秒之后输出 Timer expired

那么每次我们都要这么麻烦的设置一个时间,然后等待 channel,运行制定的函数么?

其实不是的,golfing 给我们内置了一个函数,类似于 JavaScript 的 setTimeout,那就是 AfterFunc。

函数如下:

1
func AfterFunc(d Duration, f func()) *Timer

就是他会给你返回一个 *Timer ,并在指定时间之后,在 goroutine 中运行你的函数。

上面说完了如何启动,那么怎么停止呢?很简单,就是 Stop

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   go func() {
       <- timer.C
       println("Timer expired")
  }()
   stop := timer.Stop()
   println("Timer cancelled:", stop)
}
1
Timer cancelled: true

调用停止之后,会返回一个 Bool 值:

  • true 停止成功

  • false 已经被停止或者已经到期触发

这里有一点需要注意的是,调用停止之后,并不会关闭 channel,如果你想检测那么你可以通过额外添加一个 done channel 来协助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   done := make(chan bool)
   go func() {
     select{
       case <- timer.C:
         println("Timer expired")
       case <- done:
         println("Timer stop")
    }
  }()
   stop := timer.Stop()
   done <- true
   println("Timer cancelled:", stop)
}
1
2
Timer stop
Timer cancelled: true

所以比较麻烦,大家可以自己封装一下。

Timer

单次定时器有一个特殊的方法就是 Reset,他可以将一个定时器的超时时间重新定义,这样你可以重复利用这个定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"

func main() {
   timer := time.NewTimer(time.Second)
   go func() {
     <- timer.C
     println("Timer expired")
     <- timer.C
     println("Timer expired again")
  }()
   time.Sleep(time.Second * 2)
   timer.Reset(time.Second)
   time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired again

同理,你可以用在 AfterFunc 中

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "time"

func main() {
   timer := time.AfterFunc(time.Second, func() {
     println("Timer expired")
  })
   time.Sleep(time.Second * 2)
   timer.Reset(time.Second)
   time.Sleep(time.Second * 2)
}
1
2
Timer expired
Timer expired

简直和 JavaScript 中的 setTimeout 像的不能再像了。

Ticker

这里的 Ticker 其实相当于 JavaScript 中的 setInterval,不过他没有类似 Timer 的 AfterFunc,只有一个最基础的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "time"
import "fmt"

func main() {
   ticker := time.NewTicker(time.Millisecond * 500)
   go func() {
       for t := range ticker.C {
           fmt.Println("Tick at", t)
      }
  }()
   time.Sleep(time.Millisecond * 1500)
   ticker.Stop()
   fmt.Println("Ticker stopped")
}
1
2
3
4
Tick at 2012-09-22 15:58:40.912926 -0400 EDT
Tick at 2012-09-22 15:58:41.413892 -0400 EDT
Tick at 2012-09-22 15:58:41.913888 -0400 EDT
Ticker stopped

Ticker 同样也有同 Timer 一样无法关闭 channel 的问题,解决方法和 Timer 类似,我就不多说了。

不过 Ticker 的 Stop 函数与 Timer 的不太一样,因为他没有是否触发过的问题,所以 Ticker 的 Stop是没有返回值的,这一点需要注意。

暂时先写这一个,以后如果有新的,持续更新。

注释和共享

  • 第 1 页 共 1 页

XGHeaven

一个弱弱的码农


杭州电子科技大学学生一枚


Weifang Shandong, China