概念
裝飾器本身就是<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的指標降低,很明顯二者是相悖的,因此解決這個矛盾就成了最關鍵的問題
那麼如何解決這個矛盾呢?一方面我想用,另一方面我還想要性能,這世上哪有這麼好的事情,當然要進行取捨
大家知道前端資源是動態加載的,尤其性能優化都會對首屏或者當前用不到的資源懶加載、並進行分包處理,但通用的邏輯還是會在第一時間加載。按照這種思路是不是容器也可以這樣處理?或者説有個全局容器、局部容器
將必要的通用的邏輯提取到全局的容器,而其他頁面非全局通用的局部邏輯提取到局部容器,這樣只有在局部容器的服務頁面加載時再加載便會提高全局的速度;當然,加載邏輯程序設計往往需要深思熟慮才行,這裏不過多介紹,可以自行發揮。