概念

裝飾器本身就是<u>一種特殊的函數,被用於類的各個屬性(類本身、類屬性、類方法、類訪問器、類方法的參數)</u>,裝飾器就像高階函數一樣,對目標做了一層中間操作,可以很簡潔的無痛修改一些功能和做一些有趣的功能

一個小例子:

// 日誌打印(只做代碼演示,運行時機有出入)
function logger(key: string): any {
  return function () {
    console.log("call: ", key);
  };
}

class HTTP {
  @logger("get")
  static get(url?: string) {
    return url;
  }
}
HTTP.get();  // 打印 call: get

上面簡單的演示了調用get方法時打印logger的功能,只需要在指定的屬性前方加上@logger即可,對原有的業務功能0侵入,這就是裝飾器的強大,如果以傳統的方式必然會在get內部寫一些邏輯

💡與Java註解的異同點:

  • 共同點:
  • 都是作為AOP編程範式的橫切點
  • 都是給目標的注入一些額外的元數據,方便擴展其他功能
  • 都可以通過容器進行元數據自由存取
  • 不同點:
  • 運行時機:Typescript的裝飾器函數只在運行時運行,而Java的註解在編譯時會生成對應的元數據信息,運行時階段通過反射獲取對應目標
  • 類型:Typescript的裝飾器在編譯後類型信息會被抹去,運行時無法獲取到對應的類型信息,除非使用reflect-matadata(本質也是存放數據而已);而Java的類型會存放在字節碼中,因此Java的註解是強類型的、靜態性強

環境配置

ts中的裝飾器是ES提案的一種實驗性實現,使用它需要進行一些配置,在tsconfig.json中修改配置:

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

裝飾器類別

ts的裝飾器只能用於類中,所以就有類、類方法、類屬性、類訪問器屬性、類方法參數裝飾器,接下來我們就一個個介紹

在介紹裝飾器前,先介紹個工具庫,在裝飾器中反射往往發揮着很大作用,reflect-metadata庫通常在裝飾器中都會使用,接下來介紹下其基本作用

Reflect-Metadata

嚴格地説,元數據和裝飾器是EcmaScript中兩個獨立的部分。 然而,如果你想實現像是反射)這樣的能力,你總是同時需要它們。有了reflect-metadata的幫助, 我們可以獲取編譯期的類型。
藉助reflect-metadata,運行時默認可以拿到的類型有三種:

  • design:type:屬性類型
  • design:paramtypes:方法的參數類型
  • design:returntypes:方法返回值的類型
function GetPropertyMetaType() {
  return function(target: object, key: string) {
    return Reflect.getMetadata("design:type", target, key);
  }
}

function GetReturnMetaType() {
  return function(target: object, key: string, descriptor: PropertyDescriptor) {
    return Reflect.getMetadata("design:returntypes`", target, key);
  }
}

function GetParamMetaType() {
  return function(target: object, key: string, paramIdx: number) {
    return Reflect.getMetadata("design:paramtypes`", target, key);
  }
}

class TestMeta {
  @GetPropertyMetaType()
  name: string;

  @GetReturnMetaType()
  getUser(@GetParamMetaType() name: string): number {
    return 0;
  }
}

這三種方式拿到的結果都是構造函數(例如String和Number)。規則是:

  • number -> Number
  • string -> String
  • boolean -> Boolean
  • void/null/never -> undefined
  • Array/Tuple -> Array
  • Class -> Construtor
  • Enum -> 如果是純數字枚舉為Number,否則為Object
  • Function -> function
  • 其餘都是Object

除此之外還可以自定義一些其他的附加信息:

@Reflect.metadata(metadataKey, metadataValue) // 聲明式定義元數據
class TestMeta {
  @Reflect.metadata(metadataKey, metadataValue)
  name: string;

  getUser(@Reflect.metadata(metadataKey, metadataValue) name: string): number {
    return 0;
  }
}

// 命令式定義
Reflect.defineMetadata(metadataKey, metadataValue, TestMeta.prototype, "method");

// 獲取元數據
let metadataValue = Reflect.getMetadata(metadataKey, ins, "method");

類裝飾器

類型:

type ClassDecorator = <Func extends Function>(target: Func) => Func | void;

參數:

  • @target:類的構造器
  • @return:如果有值將會替代原有的類構造器的聲明;或不返回值也可以修改原有的類

用途:

類裝飾器可以繼承現有類添加或修改一些屬性或方法

// 擴展一個toString方法
type Consturctor = { new (...args: any[]): any };

function toString<T extends Consturctor>(target: T): T {
  return class extends target {
    public toString() {
      return JSON.stringify(this);
    }
  };
}

@toString
class Car {
  constructor(public prize: number, public name: string) {}
}

// ts不會智能的推導出toString方法
console.log(new Car(1000, "BMW").toString()); // {"prize":1000,"name":"BMW"}

屬性裝飾器

類型:

type PropertyDecorator = (target: Record<string|symbol, any>, prop: string | symbol) => any | void

參數:

  • @target:對於實例屬性成員是類的原型鏈,對於靜態屬性是類的構造器
  • @prop:當前屬性名稱
  • @return:返回的值將被忽略

用途:

屬性裝飾器可以收集信息,反射賦值,給類添加方法等等,下面介紹一個完整的例子

import "reflect-metadata"; // 這裏需要藉助一個反射庫

type Constructor = { new (...args: any): any }

// 用來管理所有可注入的service
const services: Map<Constructor, Constructor> = new Map();

// 注入裝飾器
function Inject<T extends Constructor>(target: T) {
  services.set(target, target);
}
// 獲取注入的service裝飾器
function Service(target: Record<string|symbol, any>, key: string | symbol) {
  const service = services?.get(Reflect.getMetadata("design:type", target, key));
  service && (target[key] = new service());
}

// 常量裝飾器
function constant(value: any) {
  return function (target: object, key: string) {
    Object.defineProperty(target, key, {
      enumerable: false,
      configurable: false,
      get() {
        return value;
      },
      set() {
        return value;
      },
    });
  };
}

// 用户相關Service
// 讓當前service變成可注入
@Inject
class UserService {
        // 模擬獲取用户信息
  public getUser(...args: any) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("user")
      }, 1000)
    })
  }
}

// 用户頁面
class UserPage extends React.Component{
  @Service  // 注入UserService
  public service: UserService;

  // 讓BASE_URL變成常量
  @constant(process.env.BASE_URL || 'https://www.baidu.com')
  private _BASE_URL: string;

  public getUser() {
    console.log(this.service)
    return this.service?.getUser({ BASE_URL: this._BASE_URL })
  }

  render() {
    return <>
      <button onClick={this.getUser}>獲取用户信息</button>
    </>
  }
}

進階與感悟

你是不是已經通過以上講解對裝飾器的各種想法已經蠢蠢欲動了,確實通過註解形式可以很方便無痛做一些通用的功能,其基本的核心原理還是在於元數據的存取、然後通過攔截的形式修改或者附加一些額外功能

因此,使用裝飾器我們需要準備一個容器來存放元數據;如果在一個大型項目中使用,元數據過多就會導致容器過大,資源加載的時間也就會變長,尤其是在前端頁面這種加載時間是衡量用户體驗的一種很關鍵指標。我們知道前端工程打包通常都會進行treeshaking來優化掉沒有使用的代碼來減小資源體積,而使用裝飾器則必須將資源存放在容器中,這就導致treeshaking的指標降低,很明顯二者是相悖的,因此解決這個矛盾就成了最關鍵的問題

那麼如何解決這個矛盾呢?一方面我想用,另一方面我還想要性能,這世上哪有這麼好的事情,當然要進行取捨

大家知道前端資源是動態加載的,尤其性能優化都會對首屏或者當前用不到的資源懶加載、並進行分包處理,但通用的邏輯還是會在第一時間加載。按照這種思路是不是容器也可以這樣處理?或者説有個全局容器局部容器

將必要的通用的邏輯提取到全局的容器,而其他頁面非全局通用的局部邏輯提取到局部容器,這樣只有在局部容器的服務頁面加載時再加載便會提高全局的速度;當然,加載邏輯程序設計往往需要深思熟慮才行,這裏不過多介紹,可以自行發揮。