彻底搞懂装饰器
前言
每次加班到深夜都会倍感疲劳,但是屋漏偏逢连夜雨,测试环境没测出来的问题在线上被测试发现了,当然这个主人公不是我,是我的同事,本着互相帮助的原则我去看了看他的问题,哦,原来是忘记做防抖节流了;但是问题的关键在于很多地方都要加上这个防抖节流,还不知道会不会影响到其他的逻辑;于是我建议他使用装饰器,可是他连怎么用都不知道,他没有采用我的建议,我只能长叹一口气;
为什么防抖节流最适合用装饰器呢?因为它可以与原函数完全解耦;接下来就由我来介绍一下装饰器,下次再不允许你们不会用装饰器了哦!
开启装饰器
- 第一步:安装typescript
npm i -g typescript
- 第二步:初始化
tsc --init
,得到tsconfig.json文件 - 第三步:开启装饰器,
"experimentalDecorators": true,
- 第四步:开始愉快地使用装饰器......
装饰器的分类
装饰器分为以下四类:
- 类的装饰器
- 方法装饰器
- 属性装饰器
- 参数装饰器
- 访问器装饰器 下面来详细分析一下这四类装饰器:
类的装饰器
先定义一个类WebApp,然后decoratorClass这个方法去装饰器这个类,代码如下:
@decoratorClass
class WebApp{
}
function decoratorClass(){
}
类的装饰器只能接收一个参数,就是这个类的构造函数,通过构造函数可以修改原型上的属性和方法:
function decoratorClass(target: Function) {
target.prototype.name = "装饰器模式";
target.prototype.getName = function () {
return this.name;
};
}
console.log(new WebApp()); // 存在name属性和getName方法
但是装饰器上修改的原型可以被类中定义所覆盖,因为装饰器函数是在类实例化之前就执行,即使不实例化它也会执行
,比方说:
@decoratorClass
class WebApp{
name = "我被覆盖了";
}
类的装饰器还有什么特异功能呢?这就需要我们回忆一下日常的开发过程中是不是有用过类的装饰器,比如说React的类组件、Vue的类组件,我们来举例回忆一下:
@withRouter
class HelloWorld extends Component{
render(){
return null;
}
}
是不是满满的回忆,现在可能已经见不到这种React类组件了,但是这种写法可以回忆一下,其实就是代替了withRouter(HelloWorld)
让我们看一下上一个案例通过babel编译后的结果,有助于我们看清装饰器的本质:
var _class;
let WebApp = decoratorClass(_class = class WebApp {}) || _class;
function decoratorClass() {}
看到这个我们就懂了,总结一下类装饰器:如果装饰器函数有返回值,那么最终结果就是这个返回值,否则返回原来的类
方法装饰器
方法装饰器和Object.defineProperty的参数一模一样:target,key,descriptor;target指向实例对象,key表示方法名,descriptor表示属性描述符,这三个参数里面descriptor的作用最大;
首先通过descriptor.value能够获取到该方法的函数体,其次可以对函数值进行修改,比方说做一个节流防抖;给你一个WebApp类以及onChange方法,现在需要对onChange方法进行防抖节流:
class WebApp{
onChange(){
console.log("节流防抖!")
}
}
// 测试用例
const webApp = new WebApp();
setInterval(()=>{
webApp.onChange()
},10)
这是普通的写法:
onChange(){
if(this.timer){
return;
}
this.timer = setTimeout(()=>{
console.log("节流防抖");
this.timer = null;
},10000)
}
但是这样每次都需要整个函数的逻辑都要修改掉,如果说直接把节流函数提取出来作为公共方法,这个时候经常会有同学搞不清楚是应该调用还是不调用,应该放在什么地方,导致最终节流函数没有体现出节流的作用,却留下了一个bug,为日后埋下一个雷;所以更好的方法是把节流函数与业务函数解耦,使用方法装饰器就能够办到这一点:
class WebApp {
timer: any = null;
@throttle(10000)
onChange() {
console.log("节流防抖");
}
}
function throttle(interval: number) {
return (target: any, propertyKey: PropertyKey, descriptor: PropertyDescriptor) => {
const originMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!target.timer) {
target.timer = setTimeout(() => {
originMethod.apply(this, args);
target.timer = null;
}, interval);
}
};
};
}
同学们,学废了了吗?
最后把这一段代码放到babel中编译之后,发现了一个真相,代码太长就不拷贝了大家有兴趣可以自行去编译一下试试:方法装饰器其实就是Object.defineProperty
,target是构造函数的原型,key是装饰的方法名,descriptor就是装饰器的返回值
属性装饰器
属性装饰器接收两个参数,一个是实例对象,一个是key名,如下:
class WebApp {
@decoratorProperty
abc = "abc";
}
function decoratorProperty(target: WebApp, key: any) {
console.log(target, key);
}
属性装饰器可以让一个属性对应两个值,比如说下面这个案例摘自TypeScript官网:
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class WebApp{
@format("yyyy-MM-dd hh:mm:ss")
day!:number;
constructor(day:number){
this.day = day;
}
getDay(){
const day = getFormat(this, "day");
console.log(day)
}
}
const webApp = new WebApp(1);
console.log(webApp.day);
webApp.getDay();
day这个key对应着实例上的day属性,也对应元数据中的一个值,即一个key对应着两个值,reflect-metadata这个库在框架源码中经常会被引用到,这个库是用来操作元数据的,通常用来做类型校验,比如:
- 通过
Reflect.getMetadata('design:type', target, key);
获取属性的类型 design:paramtypes
获取函数参数类型,design:returntype
获取函数返回值类型- 实现控制反转和依赖注入
最后再来编译一把看看庐山真面目,一段代码总结一下,与方法装饰器不同的点就是descriptor是内置的:
Object.defineProperty(_class.prototype,key,{
configurable: true,
enumerable: true,
writable: true,
initializer: function () {
return "abc";
}
})
参数装饰器
参数装饰器和其他装饰器略有不同,最后一个参数是参数所在位置的索引:
class WebApp{
constructor(@decoratorParams name){
console.log("name:",name)
}
}
function decoratorParams(target: WebApp, key: any, index: number) {
console.log(target, key, index);
}
参数装饰器更多的是辅助其他装饰器,比如预先针对某个参数名设置了一些元数据,那么这个时候就可以在参数装饰器中通过target和key参数来获取到;
最后通过编译之后的结果来看一看参数装饰器的本质是什么?由于参数装饰器在babel平台上并不会被编译,我们换到typescript playground中编译,得到的代码如下:
最终还是落到Object.defineProperty上面去了,只不过这个参数位置是由编译器编译出来的,这个过程也比较简单,直接转化成ast,然后读取函数参数就可以拿到参数的位置,感兴趣的可以尝试着用ast解析出参数位置。
访问器装饰器
访问器装饰器与方法装饰器类似,参数与Object.defineProperty参数一致,这里就不过多介绍了
元数据
元数据就是上文提到的metadata,要使用这个特性需要引入一个库:reflect-metadata
它的主要功能就是无侵入地在类上面设置值,因为在类上直接访问是访问不到的,必须要通过Reflect.getMetadata
才能获取到值
控制反转和依赖注入
利用装饰器可以实现控制反转和依赖注入,那么问题来了,什么是控制反转和依赖注入呢?控制反转和依赖注入是SOLID中的一个设计原则,它的原理就是增加了一个容器层,将类与类之间解耦,类似于发布订阅模式,比如说B类需要用到A类的name属性,一般会这么写:
class A{
name:string;
constructor(){
this.name = 'XiaoMing';
}
}
class B{
name:string;
constructor(){
this.name = new A().name;
}
}
当A类需要传入name参数,来初始化name属性,此时B类就会报错,也必须要修改,这样的话A类与B类是强耦合:
class App{
name:string;
constructor(name){
this.name = name;
}
}
class Bpp{
name:string;
constructor(name){
this.name = new App(name).name;
}
}
程序设计的一个基本原则就是松耦合,这就要借助一个容器,来存放所有实例化的对象,然后需要使用的时候就从容器中取就可以了;假如用一个Map去存储实例化对象,举个简单的例子:
class App{
constructor(){
this.name = "Li";
}
}
const container = new Map();
container.set("App",new App())
class Bpp{
app:App;
constructor(){
this.app = container.get("App");
}
}
当然中间实例化的过程也可以抽象成一个类的装饰器,这里就不详细讲了,有兴趣的可以看一看inversify或者nest的具体实现
后记
以上就是关于装饰器的全部内容,不当之处大家可以指出来一起探讨探讨;
转载自:https://juejin.cn/post/7268593569782218807