添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
快乐的板凳  ·  android - androidx ...·  9 月前    · 
乐观的针织衫  ·  SQL server ...·  1 年前    · 
首发于 博客
TS装饰器指北

TS装饰器指北

装饰器是 JS stage-2 的一个提案,并作为 TS 的实验特性存在。如果你有使用过 Spring 的经验,相信你一定对其中强大的注解能力印象深刻,借助装饰器强大元编程能力也可以在做到类似的功能。比如 Nestjs 就是基于装饰器特性构建的。本文就来聊聊 TS 装饰器(和 JS 装饰器不是一回事~~)。

装饰器模式

在正式聊装饰器之前,先说说装饰器模式,装饰器模式的特点是: 在不改变对象自身结构的前提下,向对象添加新的功能

举个 ,一个人,可以在冬天的时穿羽绒服,也可以在下雨天套上雨衣。所有这些外在的服装并没有改变人的本质,但是它们却拓展了人的基本抗性。

class Person {
  intro() {
    console.log("我是一个帅逼");
new Person().intro(); // 我是一个帅逼
// 获取原本的行为
const origin1 = Person.prototype.intro;
// 添加羽绒服
Person.prototype.intro = function () {
  origin1.call(this);
  console.log("我穿了一件羽绒服");
new Person().intro(); // 我是一个帅逼,我穿了一件羽绒服
// 获取原本的行为,这里的行为已经是被装饰过的
const origin2 = Person.prototype.intro;
// 添加雨衣
Person.prototype.intro = function () {
  origin2.call(this);
  console.log("我穿了一件雨衣");
new Person().intro(); // 我是一个帅逼,我穿了一件羽绒服,我穿了一件羽雨衣

这个过程就像是俄罗斯套娃一样,我们没有改变原本的功能,而是为它包装了一层新的功能。



而 TS 中的装饰器达到的效果和装饰器模式不能说一模一样,只能说完全相同 。只是以一种更加优雅的方式实现而已。

function wearDownCoat(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
  const origin = descriptor.value;
  descriptor.value = function () {
    origin.call(this);
    console.log("我穿了一件羽绒服");
function wearRainCoat(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
  const origin = descriptor.value;
  descriptor.value = function () {
    origin.call(this);
    console.log("我穿了一件雨衣");
class Person {
  // 这就是装饰器,很简洁有木有 
  @wearRainCoat
  @wearDownCoat
  intro() {
    console.log("我是一个帅逼");
new Person().intro(); // 我是一个帅逼,我穿了一件羽绒服,我穿了一件羽雨衣

装饰器

TS 中装饰器使用 @expression 这种模式,装饰器本质上就是 函数 。装饰器可以作用于: 1. 类声明 2. 方法 3. 访问器( getter/setter ) 4. 属性 5. 方法参数

开启装饰器特性,需要在 tsconfig.json 中开启 experimentalDecorators

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
}

先来快速认识一下这 5 种装饰器:

// 类装饰器
@classDecorator
class Person {
  // 属性装饰器
  @propertyDecorator
  name: string;
  // 方法装饰器
  @methodDecorator
  intro(
    // 方法参数装饰器
    @parameterDecorator words: string
  // 访问器装饰器
  @accessDecorator
  get Name() {}
// 此时的 Person 已经是被装饰器增强过的了
const p=new Person()
装饰器只在解释执行时应用一次,比如上面的例子中,在完成 Person 的声明后,就已经应用了装饰器,之后的所有实例化都是增强过的 Person。

五大巨头

类装饰器

类装饰器可用于继承现有的类,或者为现有类添加属性和方法。其类型声明如下:

type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
  • 参数
  • target :类的构造函数
  • 返回值:如果类装饰器返回了一个非空的值,那么该值将用来替代原本的类

举个 ,为 Person 类添加 run 方法:

type Constructor = new (...args: any[]) => Object;
function addRun(target: Constructor) {
  // 返回一个继承 Person 的子类
  return class extends target {
    run() {
      console.log("我在狂奔");
  // 或者直接修改其 prototype,也能实现同样的效果
  //   target.prototype.run = function () {
  //     console.log("我在狂奔");
  //   };
@addRun
class Person {}
new Person().run(); // 我在狂奔

看起来似乎很棒,但是(总有那么一个但是)。TS 无法为装饰器提供类型保护,这是一个 已知的 bug,已经提了好几年了 ,它无法感知我们对 Person 做了何种修改,因此在调用 run 方法时会报错:



毕竟装饰器其实一个很动态的特性,似乎和 TS 的类型系统原则相违背。为了解决这种报错,可以简单的直接加上 @ts-ignore 就行,毕竟



更合理一点的方法是额外提供一个类声明用于提供类型信息(还是有点奇奇怪怪 ):

declare class Decorator {
  run(): void;
@addRun
class Person extends Decorator {}
new Person().run();

方法装饰器

方法装饰器可用于修改方法原本的实现。其类型声明如下:

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
  • 参数
  • target :修饰静态方法时,是类的构造方法;否则是类的原型( prototype
  • propertyKey : 方法名
  • descriptor :方法的描述对象
  • 返回值:如果方法装饰器返回了一个非空的值,那么该值将用来替代方法原本的描述对象

举个 ,为 Person 类的 run 方法添加输出其运行时间的功能:

function addLog(target: any, key: string, descriptor: PropertyDescriptor) {
  const origin = descriptor.value;
  descriptor.value = function () {
    console.time("run");
    origin.call(this);
    console.timeEnd("run");
  //   或者返回一个新的 descriptor
  //   return {
  //     ...descriptor,
  //     value: function () {
  //       console.time("run");
  //       origin.call(this);
  //       console.timeEnd("run");
  //     },
  //   };
class Person {
  @addLog
  run() {
    console.log("我在狂奔");
new Person().run(); // run: 0.063ms

访问器装饰器

访问器装饰器其实本质上来说和方法装饰器几乎一样。唯一的区别就是描述对象不同。



注意, TS 不允许为一个属性的 getter 和 setter 同时设置装饰器 。其实也很好理解,访问器装饰器的描述对象本来就是同时包含了 setter getter ,如果同时设置,势必会引起冲突。

function capitalizeName(
  target: any,
  key: string,
  descriptor: PropertyDescriptor
  // descriptor 同时包含 set 和 get 方法
  const set = descriptor.set;
  descriptor.set = function (name: string) {
    set.call(this, name.toUpperCase());
class Person {
  private name: string = "";
  get Name() {
    return this.name;
  @capitalizeName
  set Name(name: string) {
    this.name = name;
const p = new Person();
p.Name = "lower case";
console.log(p.Name); // LOWER CASE

属性装饰器

属性装饰器对比之前稍有不同。之前的构造方法和方法,访问器都是在类声明后就确定了,而属性需要等到类被实例化后才能拿到具体的结果,因此多用于收集信息。其类型声明如下:

type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
  • 参数
  • target :修饰静态方法时,是类的构造方法;否则是类的原型( prototype
  • propertyKey : 方法名
  • 返回值:忽略返回结果

提问题:为什么属性装饰器没有 descriptor 参数呢?



答案前面已经提到了, 属性需要等到类被实例化后才能拿到具体的结果 ,而装饰器实在类声明完成后就应用了,此时属性都还不存在呢,哪里来的描述对象呢?

单独使用属性装饰器意义不大,多用于和其他装饰器打配合。属性装饰器负责收集信息,其他装饰器使用这些信息。

参数装饰器

参数装饰器与属性装饰器类似,单独使用做的事情很有限,主要也是用来收集信息。和属性装饰器这个哥们属于是难兄难弟。其类型声明如下:

type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
  • 参数
  • target :修饰静态方法时,是类的构造方法;否则是类的原型( prototype
  • propertyKey : 方法名
  • parameterIndex :该参数在方法中入参中所在的位置
  • 返回值:忽略返回结果

后续有例子讲解属性装饰器和参数装饰器,这里不再展开。

简单总结一下 5 种装饰器:

| 装饰器类型 | 参数 | 返回值 | | :------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | | 类装饰器 | 1. target : 类的构造函数 | 如果类装饰器返回了一个非空的值,那么该值将用来替代原本的类 | | 方法装饰器 | 1. target : 修饰静态方法时是类构造函数;否则是类原型( prototype
2. propertyKey : 方法名
3. descriptor :方法的描述对象 | 如果方法装饰器返回了一个非空的值,那么该值将用来替代方法原本的描述对象 | | 访问器装饰器 | 1. target : 修饰静态方法时是类构造函数;否则是类原型( prototype
2. propertyKey : 方法名
3. descriptor :方法的描述对象 | 如果方法装饰器返回了一个非空的值,那么该值将用来替代方法原本的描述对象 | | 属性装饰器 | 1. target :修饰静态方法时是类构造函数;否则是类原型( prototype
2. propertyKey : 方法名 | 忽略返回结果 | | 方法参数装饰器 | 1. target :修饰静态方法时是类构造函数;否则是类原型( prototype
2. propertyKey : 方法名
3. parameterIndex :该参数在方法入参中所在的位置 | 忽略返回结果 |

装饰器工厂

有些装饰器的功能可能只有细微不同,就像文章开头提到的羽绒服和雨衣的例子一样。这个时候写多个装饰器不符合 DRY 原则 ,那么可以借助装饰器工厂简化代码。装饰器工厂本质也是函数,它会返回装饰器表达式供装饰器运行时调用。简而言之, 装饰器工长就是返回装饰器表达式的函数 ,又有点套娃的感觉了。

// 这就是装饰器工厂
function wearSomething(clothes: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const origin = descriptor.value;
    descriptor.value = function () {
      origin.call(this);
      console.log(`我穿了一件${clothes}`);
class Person {
  @wearSomething("雨衣")
  @wearSomething("羽绒服")
  intro() {
    console.log("我是一个帅逼");
new Person().intro(); // 我是一个帅逼,我是一个帅逼,我穿了一件羽雨衣

装饰器执行顺序

我们可以对同一属性应用多个装饰器,他们的顺序是:

  1. 先从外层到内层求值装饰器(如果是函数工厂的话)
  2. 应用装饰器时,是从内层到外层
function fn(str: string) {
  console.log("求值装饰器:", str);
  return function () {
    console.log("应用装饰器:", str);
function decorator() {
  console.log("应用其他装饰器");
class T {
  @fn("外层")
  @decorator
  @fn("内层")
  method() {}
}

代码将会输出:

求值装饰器: 外层 
求值装饰器: 内层 
应用装饰器: 内层
应用其他装饰器 
应用装饰器: 外层

对于不同的类型的装饰器的顺序也有明确的规定:

  1. 首先,根据书写先后,顺序执行实例成员(即 prototype )上的所有装饰器。对于同一方法来说,一定是先 应用 参数装饰器,再 应用 方法装饰器(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)
  2. 执行静态成员上的所有装饰器,顺序与上一条一致(参数装饰器 -> 方法 / 访问器 / 属性 装饰器)
  3. 执行构造方法上的所有装饰器(参数装饰器 -> 类装饰器)
function fn(str: string) {
  console.log("求值装饰器:", str);
  return function () {
    console.log("应用装饰器:", str);
@fn("类装饰器")
class T {
  constructor(@fn("类参数装饰器") foo: any) {}
  @fn("静态属性装饰器")
  static a: any;
  @fn("属性装饰器")
  b: any;
  @fn("方法装饰器")
  methodA(@fn("方法参数装饰器") foo: any) {}
  @fn("静态方法装饰器")
  static methodB(@fn("静态方法参数装饰器") foo: any) {}
  @fn("访问器装饰器")
  set C(@fn("访问器参数装饰器") foo: any) {}
  @fn("静态访问器装饰器")
  static set D(@fn("静态访问器参数装饰器") foo: any) {}
}

代码将会输出:

求值装饰器: 属性装饰器
应用装饰器: 属性装饰器
求值装饰器: 方法装饰器
求值装饰器: 方法参数装饰器
应用装饰器: 方法参数装饰器
应用装饰器: 方法装饰器
求值装饰器: 访问器装饰器
求值装饰器: 访问器参数装饰器
应用装饰器: 访问器参数装饰器
应用装饰器: 访问器装饰器
求值装饰器: 静态属性装饰器
应用装饰器: 静态属性装饰器
求值装饰器: 静态方法装饰器
求值装饰器: 静态方法参数装饰器
应用装饰器: 静态方法参数装饰器
应用装饰器: 静态方法装饰器
求值装饰器: 静态访问器装饰器
求值装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器参数装饰器
应用装饰器: 静态访问器装饰器
求值装饰器: 类装饰器
求值装饰器: 类参数装饰器
应用装饰器: 类参数装饰器
应用装饰器: 类装饰器

关门上源码

我们已经从表现层面讲解了装饰器的使用方法以及执行顺序。可能有点抽象,没关系,现在我们从源码层面来看看装饰器到底做了什么,从本质上了解装饰器。



上面的执行顺序示例代码将被编译成(可以跳转到 Playground 查看):

"use strict";
var __decorate =
  (this && this.__decorate) || // 通过 IIFE 声明 decorate 函数
  function (decorators, target, key, desc) {
    // 参数个数 < 3,说明是类装饰器
    // 参数个数 = 3,说明是属性装饰器
    // 参数个数 > 3,说明是方法/参数/访问器装饰器
    var c = arguments.length,
        c < 3
          ? target
          : desc === null // desc 为空,说明是方法/参数/访问器装饰器
          ? (desc = Object.getOwnPropertyDescriptor(target, key))
          : desc, // 在应用类装饰器时,r 是构造方法;在应用属性装饰器时,是 void 0;否则是对应方法/属性的描述对象
    // 如果当前环境支持 ES6 的 Reflect 特性,直接使用,否则使用 polyfill 实现相同的功能
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
      for (
        var i = decorators.length - 1;
        i >= 0;
        i-- // 从内层到外层依次应用装饰器
        if ((d = decorators[i]))
          // 下面这一段赋值操作是精髓,建议多看几遍
            (c < 3
              ? d(r) // 应用类装饰器
              : c > 3
              ? d(target, key, r) // 应用方法/参数/访问器装饰器
              : d(target, key)) || // 应用属性装饰器
            // 很关键,如果装饰器没有返回值,则使用原值,保证不会被错误赋值为 undefined
            // 注意:原值不代表不能被更新
            // 比如:即使方法/访问器没有返回值,只要它们改动了描述对象,即 r,这个 r 也是被更新过的 r, 因为 r 是引用类型
            // 但是对于参数装饰器来说,拿不到描述对象,所以 r 没法被改变
    // 如果方法/访问器装饰器有返回结果,将其作为新的描述对象应用到 target
    return c > 3 && r && Object.defineProperty(target, key, r), r;
var __param =
  (this && this.__param) ||
  function (paramIndex, decorator) {
    // 参数装饰器其实就是应用闭包,拿到其 index,返回返回新的方法作为装饰器
    // 这个方法与方法装饰器相比,不包含描述对象参数,且没有返回值
    return function (target, key) {
      decorator(target, key, paramIndex);
function fn(str) {
  console.log("求值装饰器:", str);
  return function () {
    console.log("应用装饰器:", str);
var T = /** @class */ (function () {
  // 类声明
  function T(foo) {}
  T.prototype.methodA = function (foo) {};
  T.methodB = function (foo) {};
  Object.defineProperty(T.prototype, "C", {
    set: function (foo) {},
    enumerable: false,
    configurable: true,
  Object.defineProperty(T, "D", {
    set: function (foo) {},
    enumerable: false,
    configurable: true,
  /* ---------- 类声明完成后,立即应用装饰器(装饰器执行一次的原因所在),之后实例化的对象基于装饰过的类 --------- */
  // 首先是实例成员
  // 所有的装饰器会按照 外层到内层的顺序 被组装成为一个数组
  // 装饰器工厂会在组装时完成求值操作
  __decorate([fn("属性装饰器")], T.prototype, "b", void 0);
  __decorate(
    [fn("方法装饰器"), __param(0, fn("方法参数装饰器"))],
    T.prototype,
    "methodA",
  __decorate(
    [fn("访问器装饰器"), __param(0, fn("访问器参数装饰器"))],
    T.prototype,
    "C",
  // 然后是静态成员
  __decorate([fn("静态属性装饰器")], T, "a", void 0);
  __decorate(
    [fn("静态方法装饰器"), __param(0, fn("静态方法参数装饰器"))],
    "methodB",
  __decorate(
    [fn("静态访问器装饰器"), __param(0, fn("静态访问器参数装饰器"))],
    "D",
  // 最后是类构造方法,使用其返回结果作为新的构造方法
  T = __decorate([fn("类装饰器"), __param(0, fn("类参数装饰器"))], T);
  return T;
})();

结合注释,相信聪明的你,一定没问题,skr!

小试牛刀:方法参数类型校验

方法参数类型校验代码地址: github.com/wjgogogo/ts-

TS 为代码提供了编译时的类型检查,我们希望更进一步,在运行时也添加类型检查的能力。要实现这种能力,单一的装饰器就不够用了,需要多种装饰器配合使用。

大致的思路如下:

  1. 使用参数装饰器(终于等到你)标记需要做类型检查的参数
  2. 使用方法装饰器增强原方法的功能,在运行原方法前先进行类型检查
  3. 待一切正常后,运行原有方法

代码如下:

type Validator = (value: unknown) => boolean;
// map 用于收集不同方法参数校验器
const validatorMap = new Map<string, Validator[]>();
function applyValidator(validator: Validator) {
  return function (target: any, key: string, idx: number) {
    let validators: Validator[];
    // 获取当前方法已存在的校验器
    if (validatorMap.has(key)) {
      validators = validatorMap.get(key);
    } else {
      // 如果不存在,则新增一个校验器配置到 map 中
      validators = [];
      validatorMap.set(key, validators);
    // 将新的检验器加入到数组中,数组第几项就对应第几个参数的校验器
    // 出于简化目的,假设每一个参数最多只能有一个校验器
    validators[idx] = validator;
function validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const origin = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    //   如果该方法不需要校验,则直接运行原方法
    if (!validatorMap.has(key)) {
      return origin.apply(this, args);
    const validators = validatorMap.get(key);
    //先对方法的每一个参数进行校验,遇到不符合规则的情况,直接报错
    validators.forEach((validator, idx) => {
      if (!validate) {
        return;
      if (!validator(args[idx])) {
        throw new TypeError(`Type validate failed for ${args[idx]}`);
    // 所有校验通过后再运行原方法
    return origin.apply(this, args);
const isString = applyValidator((x) => typeof x === "string");
const isNumber = applyValidator((x) => typeof x === "number");
class Person {
  @validate
  saySomething(@isString a: any, @isNumber b: any) {
    console.log("a: ", a, "b: ", b);
new Person().saySomething("str", 12); // a:  str b:  12
new Person().saySomething(12); // Type validate failed for 12
new Person().saySomething("str", "other str"); // Type validate failed for other str

通过上面的代码,就实现了一个简单的方法运行时参数类型验证的功能。

上面代码只存在一个类,所以 validatorMap 只需要保存 Person 的方法名和参数校验器即可。但实际情况会复杂很多,可能会有多个类,每个方法的参数可能会有多个校验器,存储这些信息的处理也会复杂很多。这个时候,我们就需要借助三方库助力开发。

reflect-metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。你可以查看 官网文档 获取详细的 API 说明。

reflect-metadata 内部也是以 Map 的数据结构存储元数据。最核心的一点是,不仅要存储元数据,还要存储这个元数据所作用在类或者类的方法、属性。理解了这一点,就理解了 reflect-metadata

当借助 reflect-metadata 后,上面的代码就可以进一步简化:

import "reflect-metadata";
function validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const origin = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    // 获取目标元数组
    const validators = Reflect.getMetadata("validators", target, key);
    if (!validators) {
      return origin.apply(this, args);
    validators.forEach((validator, idx) => {
      if (!validate) {
        return;
      if (!validator(args[idx])) {
        throw new TypeError(`Type validate failed for ${args[idx]}`);
    // 所有校验通过后再运行原方法
    return origin.apply(this, args);
const isString = (x: unknown) => typeof x === "string";
const isNumber = (x: unknown) => typeof x === "number";
class Person {
  @validate
  // Reflect.metadata 方法可以很方便的用于定义各种类型装饰器
  @Reflect.metadata("validators", [isString, isNumber])
  saySomething(a: any, b: any) {
    console.log("a: ", a, "b: ", b);
}

通过 reflect-metadata ,我们就不再需要关心状态的存储,在使用 @Reflect.metadata 时,它会自动将校验器以 key-value 形式存储起来。其中:

  1. 第一个参数代表该元数据的 key 值,也是后续检索的依据
  2. 第一个参数代表该元数据的 value 值,即存储的信息
  3. Reflect 在应用装饰器时,会关联当前的类( target )和方法 ( propertyKey )。因此在 Reflect.getMetadata ,除了传 key 值外,总是需要传入所查找的类以及其方法名或者属性名

reflect-metadata 也不完美,它的最多只能关联到当前的类和方法,比如对于参数装饰器,它能关联其 target propertyKey ,标识它的 key-value 元数据是针对哪个类的哪个方法。但是没法关联参数装饰器的 index 信息,因此如果沿用参数装饰器的形式:

class Person {
  @validate
  // Reflect.metadata 方法可以很方便的用于定义各种类型装饰器
  // @Reflect.metadata("validators", [isString, isNumber])
  saySomething(
    @Reflect.metadata("validators", isString) a: any,
    @Reflect.metadata("validators", isNumber) b: any
    console.log("a: ", a, "b: ", b);
  }

最后 Reflect.getMetadata 时只能拿到 isNumber 的校验器,因为对于同一个类的同一个方法,不能有同名的 key 存在,会采用覆盖原则(毕竟用的是 Map )。

折中方法是将 index 信息以属性的形式存储到 value 中,如同后续实战代码一样。

严格来说,元数据和 TS 并没有关系,但是 TS 在 1.5+ 的版本已经支持元数据,可以通过设置 emitDecoratorMetadata 开启此功能:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
}

你信不信,开启后,代码还可以进一步简化。



import "reflect-metadata";
function validate(target: any, key: string, descriptor: PropertyDescriptor) {
  const origin = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    // 获取目标元数组
    // 通过内置的 design:paramtypes 即可拿到参数的类型
    const paramTypes = Reflect.getMetadata("design:paramtypes", target, key);
    if (!paramTypes) {
      return origin.apply(this, args);
    paramTypes.forEach((paramType, idx) => {
        !(args[idx].constructor === paramType || args[idx] instanceof paramType)
        throw new TypeError(`Type validate failed for ${args[idx]}`);
    // 所有校验通过后再运行原方法
    return origin.apply(this, args);
class Person {
  @validate
  saySomething(a: string, b: number) {
    console.log("a: ", a, "b: ", b);
}

我们甚至不需要手动添加校验器的方法装饰器,为什么能做到这一点呢?来看看此时代码的编译结果:

// metadata 提供的装饰器工厂
var __metadata =
  (this && this.__metadata) ||
  function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
      return Reflect.metadata(k, v);
var Person = /** @class */ (function () {
  function Person() {}
  Person.prototype.saySomething = function (a, b) {
    console.log("a: ", a, "b: ", b);
  __decorate(
      validate,
      // 关键所在
      // 在编译时,会自动添加以下三种类型装饰器
      __metadata("design:type", Function),
      __metadata("design:paramtypes", [String, Number]),
      __metadata("design:returntype", void 0),
    Person.prototype,
    "saySomething",
  return Person;
})();

TS 会自动添加三个类型装饰器到属性上,这三种类型分别是:

  • design:type : 装饰器所应有的属性的类型
  • design:paramtypes : 方法的参数的类型(只在方法装饰器时存在)
  • design:returntype : 方法的返回的类型(只在方法装饰器时存在)

装饰器拿到的类型都是构造函数。规则是:

| 原始类型 | 转换后类型 | | --------------- | ------------------------------------------------------------ | | number | Number | | string | String | | boolean | Boolean | | void/null/never | undefined | | array/tuple | Array | | class | 类工造函数 | | enum | 如果是数字妹枚举,则是 Number,如果是字符串枚举,则是 String,否则是 Object | | fucntion | Function | | 其他 | Object |

到现在为止,关于 TS 装饰器的所有知识点已经讲完了,我们试着上点难度,做两个有趣的实战练习。



实战 1:Route 配置自动注入

Route 配置自动注入代码地址: github.com/wjgogogo/ts-

koa 是 nodejs 开发中比较易用的服务端框架。实战 1 就来实现一个 Spring(或者说是 Nest)风格的路由自动注入功能:



整体思路如下:

  1. 使用各种装饰器收集信息
  2. controller 装饰器收集路由前缀
  3. method 装饰器收集路由信息和回调方法
  4. param 装饰器收集哪些参数需要被映射为请求中的对应的参数信息
  5. body 装饰器收集哪些参数需要被映射为 post 请求中的 body 信息
  6. 定义路由类
  7. 加载所有路由类,获取元数据信息,根据元数据生成路由配置
  8. koa 实例添加路由配置

首先实现一系列的装饰器:

// 装饰器类型
export enum DecoratorKey {
  Controller = "controller",
  Method = "method",
  Param = "param",
  Body = "body",
// 请求类型
export enum MethodType {
  Get = "get",
  Post = "post",
// 请求装饰器元数据类型
export interface MethodMetadata {
  method: MethodType;
  route: string;
  fn: Function;
// 请求参数装饰器元数据类型
export interface ParamMetadata {
  idx: number;
  key: string;
// controller 只用于收集路由前缀
export const controller = (prefix: string) => (target: any) => {
  Reflect.defineMetadata(DecoratorKey.Controller, prefix, target);
// method 工厂用于收集路由方法,路径,和回调方法
export const method =
  (method: string) =>
  (route: string) =>
  (target: any, key: string, descriptor: PropertyDescriptor) => {
    Reflect.defineMetadata(
      DecoratorKey.Method,
        method,
        route,
        fn: descriptor.value,
      target,
// 通过工厂生成 get 和 post 装饰器工厂
export const get = method(MethodType.Get);
export const post = method(MethodType.Post);
// 请求参数装饰器用于收集所有参数映射信息
export const param =
  (paramKey: string) => (target: any, key: string, idx: number) => {
    // 所有参数信息用数组存储,因为一个方法中可以使用多个参数映射
    const params = Reflect.getMetadata(DecoratorKey.Param, target, key) ?? [];
    params.push({
      key: paramKey,
    Reflect.defineMetadata(DecoratorKey.Param, params, target, key);
// body 参数装饰器用于收集所有参数映射信息
export const body = (target: any, key: string, idx: number) => {
  // 因为 body 信息一般赋值给一个参数就可以了,所有存储一下是第几个参数即可
  Reflect.defineMetadata(DecoratorKey.Body, idx, target, key);
};

接下来定义路由类配置信息:

@controller("/users")
export class User {
  @get("/")
  getUsers(ctx: Context) {
    ctx.body = "get all users";
  @get("/:id/:name")
  getUserById(
    @param("id") id: string,
    @param("name") name: string,
    ctx: Context
    ctx.body = `get user by id: ${id}, ${name}`;
  @post("/:id")
  updateUserById(@param("id") id: string, @body body: any, ctx: Context) {
    ctx.body = `update user by id: ${id}, ${JSON.stringify(body)}`;
}

然后加载路由配置,按照 约定大于配置 的原则,路由类通常放在统一的文件夹中,比如 controller 文件夹,程序读取并执行其中的所有文件,拿到配置信息然后进行路由组装。

import Router from "@koa/router";
import Application from "koa";
// 从简原则,我们这里通过手动 import,而不是通过读取文件的方法,其效果一致
import * as Controllers from "./controller";
import { DecoratorKey, MethodMetadata, ParamMetadata } from "./decorator";
// app 代表 koa 实例
export function loadRoutes(app: Application) {
  // 遍历所有的 controller
  Object.keys(Controllers).forEach((name) => {
    // 每一个 controller 代表一组独立的路由配置
    const router = new Router();
    const Controller = Controllers[name];
    // 获取当前类装饰器的 prefix 原数据
    const prefix = Reflect.getMetadata(DecoratorKey.Controller, Controller);
    // 新建 router 实例用于配置路由
    router.prefix(prefix);
    const Prototype = Controller.prototype;
    // 遍历类中的所有方法,获取其中的配置元数据
    Object.keys(Prototype).forEach((key) => {
      const config: MethodMetadata = Reflect.getMetadata(
        DecoratorKey.Method,
        Prototype,
      // 分别获取请求参数和 body 信息
      const params = Reflect.getMetadata(DecoratorKey.Param, Prototype, key);
      const bodyIdx = Reflect.getMetadata(DecoratorKey.Body, Prototype, key);
      // 配置路由信息
      router[config.method](config.route, (ctx, next) => {
        // 处理参数映射,别忘了最后将 ctx 和 next 传入
        config.fn(...handleArgs(ctx, params, bodyIdx), ctx, next);
    app.use(router.routes());
function handleArgs(ctx, params: ParamMetadata[] = [], bodyIdx?: number) {
  const args = [];
  params.forEach(({ idx, key }) => {
    // 请求的参数均在 params 对象上,将其映射到对应的参数位置上
    args[idx] = ctx.params[key];
  if (bodyIdx) {
    // 当使用 bodyparser 后,请求的 body 信息在 request.body 上
    args[bodyIdx] = ctx.request.body;
  return args;
}

最后,实例化 koa 实例,载入中间件和路由信息即可:

// 别忘了引入 reflect-metadata
import "reflect-metadata";
import Koa from "koa";
import body from "koa-bodyparser";
import { loadRoutes } from "./loadRoutes";
const app = new Koa();
app.use(body());
loadRoutes(app);
app.listen(3000);

铛铛,完事儿~

实战 2:迷你 Mobx 实现响应式更新

Route 配置自动注入代码地址: github.com/wjgogogo/ts-

在前端热门的框架中,大多采用了响应式更新的方式,比如 Mobx ,Mobx 自身也有 基于装饰器的使用方法 ,建议先看看官方文档。实战 2 就来实现一个简易的迷你 Mobx 框架实现(面条代码警告 )。

先看看实现的效果:

import { autoRun, computed, observable, observer } from "./observer";
// 响应式的类
@observer
class Order {
  id = 0;
  // 需要响应变化的数据
  @observable
  price = 0;
  @observable
  count = 0;
  // 类似于衍生数据
  @computed
  get amount() {
    return this.price * this.count;
const order = new Order();
// 通过 autoRun 收集回调方法和数据的依赖关系
autoRun(function idFn() {
  console.log("id:", order.id);
autoRun(function priceFn() {
  console.log("price:", order.price);
autoRun(function princeAndCountFn() {
  console.log(
    `price(${order.price}) x count(${order.count}): ${
      order.price * order.count
autoRun(function amountFn() {
  console.log("amount: ", order.amount);
// 希望在所有 observable 属性值发生改变时,自动运行依赖该数据的回调方法
order.id = 12323; // 不是响应式属性,啥也不会发生
setTimeout(() => {
  order.price = 10; // 运行 priceFn, priceAndCountFn, 以及 amountFn
}, 1000);
setTimeout(() => {
  order.count = 100; // 运行 priceAndCountFn, 以及 amountFn
}, 1000);

说一说整体思路:

  1. 修改所有 observable 属性的描述对象,将它变为访问器
  2. autoRun 执行回调时,调用如果使用了 order 的某个响应属性,就会走到 getter 访问器中,此时除了返回数据外,还需要做依赖收集,将刚回调存起来
  3. 在修改了响应属性的值后,就会走到其 setter 访问器中,此时除了更新数据外,依次将收集的回调方法一一调用
  4. 在每次执行回调前,别忘了先将回调和响应函数之前的关联取消掉,否则在每次执行回调后,就会产生越来越多的依赖关系

没错,其实就是 观察者模式

import "reflect-metadata";
enum DecoratorType {
  Observable = "observable",
  Computed = "computed",
interface ComputedMetadata {
  key: string;
  fn: Function;
// Effect 类型
interface Effect {
  execute: Function;
  deps: Set<Set<Effect>>;
let runningEffect: Effect = null;
function subscribe(observer: Set<Effect>, effect: Effect) {
  observer.add(effect); // 收集 effect 回调信息
  effect.deps.add(observer); // effect 也需要和 observer 建立联系,用于后续解绑操作
function injectObservableKeys(obj, keys: string[] = []) {
  keys.forEach((key) => {
    let value = obj[key];
    let subscribes = new Set<Effect>();
    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集
        subscribe(subscribes, runningComputed || runningEffect);
        return value;
      set(updated) {
        value = updated;
        // 复制一份新的依赖队列遍历,千万不要在原对象上遍历,因为在执行回调时,又会绑定新的依赖项,造成无限循环
        [...subscribes].forEach((effect) => effect.execute());
// observer
export function observer(target): any {
  // 拿到所有的 observable 属性名
  const observableKeys: string[] = Reflect.getMetadata(
    DecoratorType.Observable,
    target.prototype
  // 返回一个新的类
  return class extends target {
    constructor() {
      super(); // 调用 super 方法完成属性初始化
      injectObservableKeys(this, observableKeys); // 处理其中的 observable keys
// observable 用于收集所有响应式属性
export const observable = (target, key) => {
  const keys = Reflect.getMetadata(DecoratorType.Observable, target) ?? [];
  keys.push(key);
  Reflect.defineMetadata(DecoratorType.Observable, keys, target);
function cleanup(effect: Effect) {
  effect.deps.forEach((dep) => {
    dep.delete(effect);
  effect.deps.clear();
export function autoRun(fn: Function) {
  const execute = () => {
    // 双向解绑
    cleanup(effect);
    // 设置当前 effect 变量,在执行回调时,响应属性才知道当前运行的是哪个 effect
    runningEffect = effect;
    try {
      fn();
    } finally {
      runningEffect = null;
  // 每一个 effect 需要包含一个需要在依赖属性变化执行的回调,以及它所依赖的属性的 subscribe 几何
  const effect: Effect = {
    execute,
    deps: new Set(),
  // 先执行依次,建立依赖关系
  execute();
}

暂时先忽略 computed 装饰器相关代码,在代码执行后,其依赖关系如图:



后续在响应属性更新后,就会执行相应的依赖回调。

再来看看 computed 方法, computed 属性相当于一个中间层,一边对接回调方法,一边对接响应属性,在响应属性改变后,执行 computed 的属性的响应方法(类似于响应数据的 setter 方法),然后再执行所有回调方法:



因此, computed 其实和 effect 类似。加入 computed 后,完整代码如下:

import "reflect-metadata";
enum DecoratorType {
  Observable = "observable",
  Computed = "computed",
interface ComputedMetadata {
  key: string;
  fn: Function;
// Effect 类型
interface Effect {
  execute: Function;
  deps: Set<Set<Effect>>;
let runningEffect: Effect = null;
let runningComputed: Effect = null;
function subscribe(observer: Set<Effect>, effect: Effect) {
  observer.add(effect); // 收集 effect 回调信息
  effect.deps.add(observer); // effect 也需要和 observer 建立联系,用于后续解绑操作
function injectObservableKeys(obj, keys: string[] = []) {
  keys.forEach((key) => {
    let value = obj[key];
    let subscribes = new Set<Effect>();
    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集,runningComputed 优先
        subscribe(subscribes, runningComputed || runningEffect);
        return value;
      set(updated) {
        value = updated;
        // 复制一份新的依赖队列遍历,千万不要在原对象上遍历,因为在执行回调时,又会绑定新的依赖项,造成无限循环
        [...subscribes].forEach((effect) => effect.execute());
function injectComputedKeys(obj, keys: ComputedMetadata[] = []) {
  keys.forEach((computed) => {
    let subscribes = new Set<Effect>();
    const executeComputedGetter = () => {
      cleanup(effect);
      // 用另个标识标记 computed effect
      runningComputed = effect;
      try {
        return computed.fn.call(obj);
      } finally {
        runningComputed = null;
    // computed 的 execute 就是让其依赖回调都执行一遍
    const execute = () => {
      [...subscribes].forEach((effect) => effect.execute());
    const effect: Effect = {
      execute,
      deps: new Set(),
    Object.defineProperty(obj, computed.key, {
      get() {
        subscribe(subscribes, runningEffect);
        return executeComputedGetter();
// observer
export function observer(target): any {
  // 拿到所有的 observable 属性名
  const observableKeys: string[] = Reflect.getMetadata(
    DecoratorType.Observable,
    target.prototype
  const computedKeys: ComputedMetadata[] = Reflect.getMetadata(
    DecoratorType.Computed,
    target.prototype
  // 返回一个新的类
  return class extends target {
    constructor() {
      super(); // 调用 super 方法完成属性初始化
      injectObservableKeys(this, observableKeys); // 处理其中的 observable keys
      injectComputedKeys(this, computedKeys);
// observable 用于收集所有响应式属性
export const observable = (target, key) => {
  const keys = Reflect.getMetadata(DecoratorType.Observable, target) ?? [];
  keys.push(key);
  Reflect.defineMetadata(DecoratorType.Observable, keys, target);
export const computed = (target, key, descriptor) => {
  const keys = Reflect.getMetadata(DecoratorType.Computed, target) ?? [];
  // 假定所有 computed 都是 getter
  keys.push({ key, fn: descriptor.get });
  Reflect.defineMetadata(DecoratorType.Computed, keys, target);
function cleanup(effect: Effect) {
  effect.deps.forEach((dep) => {
    dep.delete(effect);
  effect.deps.clear();
export function autoRun(fn: Function) {
  const execute = () => {
    // 双向解绑
    cleanup(effect);
    // 设置当前 effect 变量
    runningEffect = effect;
    try {
      fn();
    } finally {
      runningEffect = null;
  // 每一个 effect 需要包含一个需要在依赖属性变化执行的回调,以及它所依赖的属性的 subscribe 几何
  const effect: Effect = {